Subida a producción
This commit is contained in:
parent
51bd9fe7af
commit
c349d12f9d
@ -1,9 +1,15 @@
|
||||
from typing import Any, Mapping
|
||||
|
||||
import fdb
|
||||
import mysql.connector
|
||||
|
||||
from app.config import logger
|
||||
|
||||
|
||||
def get_factuges_connection(config):
|
||||
def get_factuges_connection(config) -> fdb.Connection:
|
||||
"""
|
||||
Crea y devuelve una conexión a FactuGES (Firebird).
|
||||
"""
|
||||
try:
|
||||
conn = fdb.connect(
|
||||
host=config['FACTUGES_HOST'],
|
||||
@ -11,34 +17,53 @@ def get_factuges_connection(config):
|
||||
database=config['FACTUGES_DATABASE'],
|
||||
user=config['FACTUGES_USER'],
|
||||
password=config['FACTUGES_PASSWORD'],
|
||||
charset='UTF8'
|
||||
charset='UTF8',
|
||||
)
|
||||
logger.info(
|
||||
f"Conexión a la base de datos FactuGES establecida: {config['FACTUGES_HOST']} with database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}")
|
||||
"Conexión a la base de datos FactuGES establecida: %s with database:%s - using user:%s",
|
||||
config['FACTUGES_HOST'],
|
||||
config['FACTUGES_DATABASE'],
|
||||
config['FACTUGES_USER'],
|
||||
)
|
||||
return conn
|
||||
except Exception as e:
|
||||
logger.error("Error al conectar a la base de datos FactuGES.")
|
||||
logger.error(
|
||||
f"(ERROR) Failed to establish connection to: {config['FACTUGES_HOST']} with database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}")
|
||||
"(ERROR) Failed to establish connection to: %s with database:%s - using user:%s",
|
||||
config['FACTUGES_HOST'],
|
||||
config['FACTUGES_DATABASE'],
|
||||
config['FACTUGES_USER'],
|
||||
)
|
||||
logger.error(str(e))
|
||||
raise e
|
||||
|
||||
|
||||
def get_mysql_connection(config):
|
||||
def get_mysql_connection(config: Mapping[str, Any]):
|
||||
"""
|
||||
Crea y devuelve una conexión a MySQL.
|
||||
"""
|
||||
try:
|
||||
conn = mysql.connector.connect(
|
||||
host=config['FWEB_MYSQL_HOST'],
|
||||
port=config['FWEB_MYSQL_PORT'],
|
||||
database=config['FWEB_MYSQL_DATABASE'],
|
||||
user=config['FWEB_MYSQL_USER'],
|
||||
password=config['FWEB_MYSQL_PASSWORD']
|
||||
host=config["FWEB_MYSQL_HOST"],
|
||||
port=config["FWEB_MYSQL_PORT"],
|
||||
database=config["FWEB_MYSQL_DATABASE"],
|
||||
user=config["FWEB_MYSQL_USER"],
|
||||
password=config["FWEB_MYSQL_PASSWORD"],
|
||||
)
|
||||
logger.info(
|
||||
f"Conexión a la base de datos MySQL establecida a: {config['FWEB_MYSQL_HOST']} with database:{config['FWEB_MYSQL_DATABASE']} - using user:{config['FWEB_MYSQL_USER']}")
|
||||
"Conexión a la base de datos MySQL establecida a: %s with database:%s - using user:%s",
|
||||
config['FWEB_MYSQL_HOST'],
|
||||
config['FWEB_MYSQL_DATABASE'],
|
||||
config['FWEB_MYSQL_USER'],
|
||||
)
|
||||
return conn
|
||||
except Exception as e:
|
||||
logger.error("Error al conectar a la base de datos MySQL.")
|
||||
logger.error(
|
||||
f"(ERROR) Failed to establish connection to: {config['FWEB_MYSQL_HOST']} with database:{config['FWEB_MYSQL_DATABASE']} - using user:{config['FWEB_MYSQL_USER']}")
|
||||
"(ERROR) Failed to establish connection to: %s with database:%s - using user:%s",
|
||||
config['FWEB_MYSQL_HOST'],
|
||||
config['FWEB_MYSQL_DATABASE'],
|
||||
config['FWEB_MYSQL_USER'],
|
||||
)
|
||||
logger.error(str(e))
|
||||
raise e
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
from app.config import logger
|
||||
from typing import Dict, Any
|
||||
from typing import Any, Dict
|
||||
|
||||
from uuid6 import uuid7
|
||||
from app.config import load_config
|
||||
from . import sql_sentences as SQL
|
||||
from . import normalizations as NORMALIZA
|
||||
|
||||
from app.config import load_config, logger
|
||||
from app.utils import validar_nif
|
||||
|
||||
from . import normalizations as NORMALIZA
|
||||
from . import sql_sentences as SQL
|
||||
|
||||
|
||||
def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
|
||||
config = load_config()
|
||||
@ -26,14 +28,14 @@ def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
|
||||
if ids_verifactu_deleted:
|
||||
sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config)
|
||||
else:
|
||||
logger.info(
|
||||
f"There are NOT customer invoices deleted since the last run")
|
||||
logger.info("There are NOT customer invoices deleted since the last run")
|
||||
|
||||
except Exception as e:
|
||||
if cursor_mysql is not None:
|
||||
cursor_mysql.close()
|
||||
logger.error(f"(ERROR) Failed to fetch from database:{
|
||||
config['FWEB_MYSQL_DATABASE']} - using user:{config['FWEB_MYSQL_USER']}")
|
||||
logger.error(
|
||||
f"(ERROR) Failed to fetch from database:{config['FWEB_MYSQL_DATABASE']} - using user:{config['FWEB_MYSQL_USER']}"
|
||||
)
|
||||
logger.error(e)
|
||||
raise e
|
||||
|
||||
@ -48,8 +50,9 @@ def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
|
||||
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(
|
||||
f"(ERROR) Failed to fetch from database:{config['FACTUGES_DATABASE']} - using user:{config['FACTUGES_USER']}"
|
||||
)
|
||||
logger.error(e)
|
||||
raise e
|
||||
|
||||
@ -65,10 +68,10 @@ def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
|
||||
# Verificar si hay filas en el resultado
|
||||
if tuplas_seleccionadas:
|
||||
sync_invoices_from_FACTUGES(
|
||||
conn_mysql, tuplas_seleccionadas, conn_factuges, config)
|
||||
conn_mysql, tuplas_seleccionadas, conn_factuges, config
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"There are NOT new FACTURAS rows since the last run.")
|
||||
logger.info("There are NOT new FACTURAS rows since the last run.")
|
||||
|
||||
|
||||
def sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config):
|
||||
@ -79,8 +82,10 @@ def sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config):
|
||||
cursor_FactuGES = conn_factuges.cursor()
|
||||
if ids_verifactu_deleted:
|
||||
logger.info(f"Liberate factuGES: {ids_verifactu_deleted}")
|
||||
cursor_FactuGES.executemany(SQL.LIMPIAR_FACTUGES_LINK, [(
|
||||
id_verifactu,) for id_verifactu in ids_verifactu_deleted])
|
||||
cursor_FactuGES.executemany(
|
||||
SQL.LIMPIAR_FACTUGES_LINK,
|
||||
[(id_verifactu,) for id_verifactu in ids_verifactu_deleted],
|
||||
)
|
||||
else:
|
||||
logger.info("No articles to delete.")
|
||||
|
||||
@ -103,7 +108,7 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
|
||||
factuges_id_anterior = None
|
||||
num_fac_procesed = 0
|
||||
customer_valid = True
|
||||
invoice_id = None
|
||||
invoice_id: str | None = None
|
||||
|
||||
try:
|
||||
cursorMySQL = conn_mysql.cursor()
|
||||
@ -111,27 +116,28 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
|
||||
|
||||
# Insertar datos en la tabla 'customer_invoices'
|
||||
for factura_detalle in filas:
|
||||
|
||||
# Preparamos los campos para evitar errores
|
||||
customer_fields = NORMALIZA.normalize_customer_fields(
|
||||
factura_detalle)
|
||||
customer_fields = NORMALIZA.normalize_customer_fields(factura_detalle)
|
||||
header_invoice_fields = NORMALIZA.normalize_header_invoice_fields(
|
||||
factura_detalle)
|
||||
factura_detalle
|
||||
)
|
||||
details_invoice_fields = NORMALIZA.normalize_details_invoice_fields(
|
||||
factura_detalle)
|
||||
factura_detalle
|
||||
)
|
||||
|
||||
factuges_id = int(factura_detalle['ID_FACTURA'])
|
||||
factuges_id = int(factura_detalle["ID_FACTURA"])
|
||||
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_result = int(config["CTE_SYNC_RESULT_OK"])
|
||||
sync_notes = None
|
||||
customer_valid = validar_nif(customer_fields["tin"], customer_fields["name"], config)
|
||||
customer_valid = validar_nif(
|
||||
customer_fields["tin"], customer_fields["name"], config
|
||||
)
|
||||
if customer_valid:
|
||||
# Comprobamos si existe el cliente del primer item de la factura
|
||||
customer_id = get_or_create_customer(
|
||||
cursorMySQL,
|
||||
config['CTE_COMPANY_ID'],
|
||||
config["CTE_COMPANY_ID"],
|
||||
str(factura_detalle["ID_CLIENTE"]),
|
||||
customer_fields,
|
||||
)
|
||||
@ -146,40 +152,59 @@ 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, customer_fields, header_invoice_fields, customer_id, pm_id, str(
|
||||
factura_detalle["DES_FORMA_PAGO"]), config
|
||||
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)
|
||||
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)
|
||||
cursorMySQL, header_invoice_fields, invoice_id, config
|
||||
)
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
# 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))
|
||||
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),
|
||||
)
|
||||
num_fac_procesed += 1
|
||||
|
||||
# Insertamos detalles y taxes correspondientes siempre que hayamos insertado cabecera
|
||||
if customer_valid:
|
||||
if invoice_id is None:
|
||||
raise RuntimeError("BUG: invoice_id no debería ser None si customer_valid es True")
|
||||
insert_item_and_taxes(cursorMySQL, invoice_id, details_invoice_fields)
|
||||
|
||||
# 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)}")
|
||||
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
|
||||
@ -194,7 +219,9 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
|
||||
cursor_FactuGES.close()
|
||||
|
||||
|
||||
def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fields: Dict[str, Any]) -> str:
|
||||
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
|
||||
"""
|
||||
@ -202,15 +229,34 @@ def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fiel
|
||||
row = cur.fetchone()
|
||||
if not row or not row[0]:
|
||||
customer_id = str(uuid7())
|
||||
logger.info("Inserting customer %s %s %s",
|
||||
factuges_customer_id, fields["tin"], fields["name"])
|
||||
logger.info(
|
||||
"Inserting customer %s %s %s",
|
||||
factuges_customer_id,
|
||||
fields["tin"],
|
||||
fields["name"],
|
||||
)
|
||||
cur.execute(
|
||||
SQL.INSERT_CUSTOMER,
|
||||
(
|
||||
customer_id, fields["name"], fields["tin"], fields["street"], fields["city"], fields["province"],
|
||||
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["is_company"]
|
||||
customer_id,
|
||||
fields["name"],
|
||||
fields["tin"],
|
||||
fields["street"],
|
||||
fields["city"],
|
||||
fields["province"],
|
||||
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["is_company"],
|
||||
),
|
||||
)
|
||||
return customer_id
|
||||
@ -219,16 +265,31 @@ def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fiel
|
||||
cur.execute(
|
||||
SQL.UPDATE_CUSTOMER,
|
||||
(
|
||||
fields["name"], fields["tin"], fields["street"], fields["city"], fields["province"], fields["postal_code"],
|
||||
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,
|
||||
fields["name"],
|
||||
fields["tin"],
|
||||
fields["street"],
|
||||
fields["city"],
|
||||
fields["province"],
|
||||
fields["postal_code"],
|
||||
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,
|
||||
),
|
||||
)
|
||||
return customer_id
|
||||
|
||||
|
||||
def get_or_create_payment_method(cur, factuges_payment_id: str, description: str) -> str:
|
||||
def get_or_create_payment_method(
|
||||
cur, factuges_payment_id: str, description: str
|
||||
) -> str:
|
||||
"""
|
||||
En el caso de que la forma de pago no exista la creamos
|
||||
"""
|
||||
@ -236,73 +297,142 @@ def get_or_create_payment_method(cur, factuges_payment_id: str, description: str
|
||||
row = cur.fetchone()
|
||||
if not row or not row[0]:
|
||||
pm_id = str(uuid7())
|
||||
logger.info("Inserting payment method %s %s %s",
|
||||
factuges_payment_id, pm_id, description)
|
||||
cur.execute(SQL.INSERT_PAYMENT_METHOD,
|
||||
(pm_id, description, factuges_payment_id))
|
||||
logger.info(
|
||||
"Inserting payment method %s %s %s", factuges_payment_id, pm_id, description
|
||||
)
|
||||
cur.execute(
|
||||
SQL.INSERT_PAYMENT_METHOD, (pm_id, description, factuges_payment_id)
|
||||
)
|
||||
return pm_id
|
||||
pm_id = str(row[0])
|
||||
logger.info("Payment method exists %s -> %s", factuges_payment_id, pm_id)
|
||||
return pm_id
|
||||
|
||||
|
||||
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:
|
||||
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())
|
||||
|
||||
logger.info("Inserting invoice %s %s %s %s %s",
|
||||
invoice_id, hif.get('reference'), hif.get('invoice_date'), hif.get('operation_date'), config['CTE_STATUS_INVOICE'])
|
||||
cur.execute(
|
||||
logger.info(
|
||||
"Inserting invoice %s %s %s %s %s",
|
||||
invoice_id,
|
||||
hif.get("reference"),
|
||||
hif.get("invoice_date"),
|
||||
hif.get("operation_date"),
|
||||
config["CTE_STATUS_INVOICE"],
|
||||
)
|
||||
cur.execute( # type: ignore
|
||||
SQL.INSERT_INVOICE,
|
||||
(
|
||||
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'),
|
||||
hif.get('taxes_amount_value'), hif.get('total_amount_value'),
|
||||
customer_id, cf.get("tin"), cf.get(
|
||||
'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'), hif.get('company_id'), hif.get('is_proforma')
|
||||
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"),
|
||||
hif.get("taxes_amount_value"),
|
||||
hif.get("total_amount_value"),
|
||||
customer_id,
|
||||
cf.get("tin"),
|
||||
cf.get("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"),
|
||||
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:
|
||||
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(
|
||||
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']
|
||||
),
|
||||
(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
|
||||
"""
|
||||
# IVA (>= 0 acepta 0% también, si no quieres registrar 0, cambia condición a > 0)
|
||||
if (IVA or 0) >= 0:
|
||||
cur.execute(SQL.INSERT_INVOICE_TAX,
|
||||
(str(uuid7()), invoice_id, hif.get('tax_code'), hif.get('base'), hif.get('iva')))
|
||||
def insert_header_taxes_if_any(
|
||||
cur,
|
||||
invoice_id: str,
|
||||
IVA: str,
|
||||
RECARGO: str,
|
||||
hif: Dict[str, Any],
|
||||
) -> None:
|
||||
"""Inserta impuestos de cabecera"""
|
||||
|
||||
# Recargo equivalencia
|
||||
if (RECARGO or 0) > 0:
|
||||
cur.execute(SQL.INSERT_INVOICE_TAX,
|
||||
(str(uuid7()), invoice_id, 'rec_5_2', hif.get('base'), hif.get('re')))
|
||||
# Conversión segura a número
|
||||
try:
|
||||
iva_val = float(IVA)
|
||||
except (TypeError, ValueError):
|
||||
iva_val = 0.0
|
||||
|
||||
try:
|
||||
rec_val = float(RECARGO)
|
||||
except (TypeError, ValueError):
|
||||
rec_val = 0.0
|
||||
|
||||
# IVA (>= 0 acepta 0%)
|
||||
if iva_val >= 0:
|
||||
cur.execute(
|
||||
SQL.INSERT_INVOICE_TAX,
|
||||
(
|
||||
str(uuid7()),
|
||||
invoice_id,
|
||||
hif.get("tax_code"),
|
||||
hif.get("base"),
|
||||
hif.get("iva"),
|
||||
),
|
||||
)
|
||||
|
||||
# Recargo equivalencia (> 0)
|
||||
if rec_val > 0:
|
||||
cur.execute(
|
||||
SQL.INSERT_INVOICE_TAX,
|
||||
(
|
||||
str(uuid7()),
|
||||
invoice_id,
|
||||
"rec_5_2",
|
||||
hif.get("base"),
|
||||
hif.get("re"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def insert_item_and_taxes(cur, invoice_id: str, fields: Dict[str, Any]) -> None:
|
||||
@ -314,11 +444,23 @@ def insert_item_and_taxes(cur, invoice_id: str, fields: Dict[str, Any]) -> None:
|
||||
# logger.info("Inserting item %s pos=%s qty=%s", item_id, fields.get('position'), fields.get('quantity_value'))
|
||||
cur.execute(
|
||||
SQL.INSERT_INVOICE_ITEM,
|
||||
(item_id, invoice_id, fields.get('position'), fields.get('description'), fields.get('quantity_value'),
|
||||
fields.get('unit_value'), fields.get('discount_percentage_value'), None, fields.get('total_value'),
|
||||
fields.get('iva_code'), fields.get('iva_percentage_value'), fields.get('tax_amount'),
|
||||
fields.get('rec_code'), fields.get('rec_percentage_value'), fields.get('rec_amount'),
|
||||
)
|
||||
(
|
||||
item_id,
|
||||
invoice_id,
|
||||
fields.get("position"),
|
||||
fields.get("description"),
|
||||
fields.get("quantity_value"),
|
||||
fields.get("unit_value"),
|
||||
fields.get("discount_percentage_value"),
|
||||
None,
|
||||
fields.get("total_value"),
|
||||
fields.get("iva_code"),
|
||||
fields.get("iva_percentage_value"),
|
||||
fields.get("tax_amount"),
|
||||
fields.get("rec_code"),
|
||||
fields.get("rec_percentage_value"),
|
||||
fields.get("rec_amount"),
|
||||
),
|
||||
)
|
||||
|
||||
# logger.info("Inserting item tax %s code=%s base=%s tax=%s",
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from app.config import logger
|
||||
from typing import Dict, Any, Tuple, Optional, List, Iterable
|
||||
from app.config import load_config
|
||||
from decimal import Decimal
|
||||
from app.utils import validar_nif, estado_factura, crear_factura, TaxCatalog, unscale_to_str
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from app.config import load_config, logger
|
||||
from app.utils import TaxCatalog, crear_factura, estado_factura, unscale_to_str
|
||||
|
||||
from . import sql_sentences as SQL
|
||||
|
||||
|
||||
@ -32,9 +32,9 @@ 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)
|
||||
logger.info(f"Ok send Verifactu")
|
||||
logger.info("Ok send Verifactu")
|
||||
else:
|
||||
logger.info(f"There are no rows to send")
|
||||
logger.info("There are no rows to send")
|
||||
|
||||
except Exception as error:
|
||||
if cursor_mysql is not None:
|
||||
@ -45,40 +45,57 @@ def sync_invoices_verifactu(conn_mysql, last_execution_date):
|
||||
raise error
|
||||
|
||||
|
||||
def enviar_datos(invoices_to_verifactu, cursor_mysql, config):
|
||||
def enviar_datos(
|
||||
invoices_to_verifactu,
|
||||
cursor_mysql,
|
||||
config: Dict[str, Any]
|
||||
) -> None:
|
||||
|
||||
factura: Optional[Dict[str, Any]] = None
|
||||
invoice_id: Optional[str] = None
|
||||
|
||||
# Recorrer todas las facturas para crear json de envio
|
||||
try:
|
||||
invoice_id = None
|
||||
factura = None
|
||||
for fila in invoices_to_verifactu:
|
||||
fila_id = str(fila["id"])
|
||||
|
||||
# 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']):
|
||||
if invoice_id != fila_id:
|
||||
|
||||
procesar_factura_verifactu(factura, cursor_mysql, config)
|
||||
# Cerrar la factura anterior (si existe)
|
||||
if factura is not None:
|
||||
procesar_factura_verifactu(factura, cursor_mysql, config)
|
||||
|
||||
# preparamos nueva factura
|
||||
ok, respuesta = preparar_factura(fila)
|
||||
ok, factura_nueva, error = preparar_factura(fila)
|
||||
if not ok:
|
||||
logger.info(
|
||||
f"ERROR >>>>>> Factura {fila['reference']} no cumple requisitos para ser mandada a Verifactu:")
|
||||
logger.info(
|
||||
f">>>>>> Faltan campos requeridos: {respuesta}")
|
||||
factura = None
|
||||
logger.info(f"ERROR >>> Factura {fila['reference']} no válida: {error}")
|
||||
continue
|
||||
factura = respuesta
|
||||
|
||||
factura = factura_nueva
|
||||
invoice_id = fila_id
|
||||
|
||||
# Añadir línea a factura actual
|
||||
ok, linea = preparar_linea(fila)
|
||||
if not ok:
|
||||
ref = factura["reference"] if factura else "(desconocida)"
|
||||
logger.info(
|
||||
f"ERROR >>>>>> Factura {factura.get('reference')} no cumple requisitos para ser mandada a Verifactu:")
|
||||
f"ERROR >>>>>> Factura {ref} no cumple requisitos para ser mandada a Verifactu:")
|
||||
logger.info(f">>>>>> Faltan campos requeridos: {linea}")
|
||||
factura = None
|
||||
else:
|
||||
continue
|
||||
|
||||
# Línea válida
|
||||
# Garantizamos que factura no es None
|
||||
if factura is not None:
|
||||
factura["lineas"].append(linea)
|
||||
|
||||
procesar_factura_verifactu(factura, cursor_mysql, config)
|
||||
# Procesar la última factura
|
||||
if factura is not None:
|
||||
procesar_factura_verifactu(factura, cursor_mysql, config)
|
||||
|
||||
except Exception as e:
|
||||
# Escribir el error en el archivo de errores
|
||||
@ -92,58 +109,95 @@ def procesar_factura_verifactu(
|
||||
config: Dict[str, Any]
|
||||
) -> bool:
|
||||
|
||||
if factura != None:
|
||||
# Creamos registro de factura en verifactu
|
||||
if factura.get('uuid') == '':
|
||||
# logger.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")
|
||||
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"), qr_verifactu, factura.get("id")))
|
||||
logger.info(
|
||||
f">>> Factura {factura.get("reference")} registrada en Verifactu")
|
||||
return True
|
||||
else:
|
||||
logger.info(
|
||||
f">>> Factura {factura.get("reference")} enviada a Verifactu con error {respuesta}")
|
||||
return False
|
||||
# Actualizamos registro de factura en verifactu
|
||||
if factura is None:
|
||||
return False
|
||||
|
||||
# Creamos registro de factura en verifactu
|
||||
if factura.get('uuid') == '':
|
||||
# logger.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") or {}
|
||||
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"),
|
||||
qr_verifactu,
|
||||
factura.get("id"),
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
f">>> Factura {factura.get('reference')} registrada en Verifactu")
|
||||
return True
|
||||
else:
|
||||
# logger.info(f"Send to update Verifactu: {factura}")
|
||||
respuesta = estado_factura(factura.get('uuid'), config)
|
||||
if respuesta.get("status") == 200 and respuesta.get("ok"):
|
||||
data = respuesta.get("data")
|
||||
cursor_mysql.execute(SQL.update_verifactu_records_with_uuid, (data.get(
|
||||
'estado'), data.get('operacion'), factura.get('uuid')))
|
||||
logger.info(
|
||||
f">>> Factura {factura.get("reference")} actualizado registro de Verifactu")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
logger.info(
|
||||
f">>> Factura {factura.get('reference')} enviada a Verifactu con error {respuesta}")
|
||||
return False
|
||||
|
||||
# Actualizamos registro de factura en verifactu
|
||||
|
||||
# logger.info(f"Send to update Verifactu: {factura}")
|
||||
uuid_val = factura.get("uuid")
|
||||
if not uuid_val: # None, '', o falsy
|
||||
logger.error(f"Factura {factura.get('reference')} sin UUID válido")
|
||||
return False
|
||||
|
||||
respuesta = estado_factura(uuid_val, config)
|
||||
|
||||
if respuesta.get("status") == 200 and respuesta.get("ok"):
|
||||
data = respuesta.get("data") or {}
|
||||
cursor_mysql.execute(
|
||||
SQL.update_verifactu_records_with_uuid,
|
||||
(data.get('estado'),
|
||||
data.get('operacion'),
|
||||
uuid_val)
|
||||
)
|
||||
logger.info(
|
||||
f">>> Factura {factura.get('reference')} actualizado registro de Verifactu")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def preparar_factura(fila: Dict[str, Any]) -> Tuple[bool, Dict[str, Any] | list]:
|
||||
def preparar_factura(
|
||||
fila: Dict[str, Any]
|
||||
) -> Tuple[bool, Dict[str, Any] | None, str | None]:
|
||||
"""
|
||||
Prepara el JSON de factura para Verifactu a partir de 'fila'.
|
||||
|
||||
Retorna:
|
||||
(ok, factura, error)
|
||||
"""
|
||||
campos_requeridos = ("series", "invoice_number",
|
||||
"invoice_date", "description", "total_amount_value")
|
||||
|
||||
campos_requeridos = (
|
||||
"series", "invoice_number", "invoice_date",
|
||||
"description", "total_amount_value"
|
||||
)
|
||||
|
||||
ok, missing = validar_requeridos(fila, campos_requeridos)
|
||||
|
||||
if not ok:
|
||||
return False, missing
|
||||
# Devolvemos string, NO lista
|
||||
return False, None, ", ".join(missing)
|
||||
|
||||
factura = {
|
||||
"nif": fila['customer_tin'],
|
||||
"nombre": fila['customer_name'],
|
||||
"serie": fila['series'],
|
||||
"numero": fila['invoice_number'],
|
||||
|
||||
# convertimos la fecha al formato requerido en el api
|
||||
"fecha_expedicion": fila['invoice_date'].strftime("%d-%m-%Y"),
|
||||
|
||||
# F1: Factura (Art. 6, 7.2 Y 7.3 del RD 1619/2012)
|
||||
"tipo_factura": "F1",
|
||||
"descripcion": fila['description'],
|
||||
|
||||
# desescalamos el importe para dar su importe real
|
||||
"importe_total": unscale_to_str(str(fila['total_amount_value']), str(fila['total_amount_scale'])),
|
||||
"lineas": [],
|
||||
@ -154,7 +208,7 @@ def preparar_factura(fila: Dict[str, Any]) -> Tuple[bool, Dict[str, Any] | list]
|
||||
"uuid": fila['uuid'],
|
||||
}
|
||||
|
||||
return True, factura
|
||||
return True, factura, None
|
||||
|
||||
|
||||
def preparar_linea(fila: Dict[str, Any]) -> Tuple[bool, Dict[str, Any] | list]:
|
||||
|
||||
@ -17,7 +17,7 @@ def send_orders_mail(inserted_orders):
|
||||
for order in inserted_orders:
|
||||
send_smtp_email = brevo_python.SendSmtpEmail(
|
||||
to=[{'email': config['MAIL_TO']}],
|
||||
subject=f"Nuevo pedido del distribuidor {order["dealer_name"]}",
|
||||
subject=f"Nuevo pedido del distribuidor {order['dealer_name']}",
|
||||
template_id=int(config["BREVO_EMAIL_TEMPLATE"]),
|
||||
params={
|
||||
"customer_reference": order["customer_reference"],
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
from app.config import logger
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
|
||||
|
||||
def estado_factura(uuid_str: str,
|
||||
config,
|
||||
) -> Tuple[bool, Optional[Dict[str, Any]], Optional[str]]:
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Llama al endpoint de Verifacti para cosultar el estado de registro.
|
||||
|
||||
@ -52,15 +54,15 @@ def estado_factura(uuid_str: str,
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error("Error de conexión con la API Verifacti: %s", e)
|
||||
return False, None, str(e)
|
||||
return {"ok": False, "status": 500, "error": str(e)}
|
||||
except ValueError as e:
|
||||
logger.error("Respuesta no es JSON válido: %s", e)
|
||||
return False, None, "Respuesta no es JSON válido"
|
||||
return {"ok": False, "status": 500, "error": "Respuesta no es JSON válido"}
|
||||
|
||||
|
||||
def crear_factura(payload,
|
||||
config,
|
||||
) -> Tuple[bool, Optional[Dict[str, Any]], Optional[str]]:
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Llama al endpoint de Verifacti para crear una factura.
|
||||
|
||||
@ -107,10 +109,10 @@ def crear_factura(payload,
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error("Error de conexión con la API Verifacti: %s", e)
|
||||
return False, None, str(e)
|
||||
return {"ok": False, "status": 500, "error": str(e)}
|
||||
except ValueError as e:
|
||||
logger.error("Respuesta no es JSON válido: %s", e)
|
||||
return False, None, "Respuesta no es JSON válido"
|
||||
return {"ok": False, "status": 500, "error": "Respuesta no es JSON válido"}
|
||||
|
||||
|
||||
def validar_nif(
|
||||
@ -140,7 +142,7 @@ def validar_nif(
|
||||
resp = requests.post(
|
||||
url, json=payload, headers=headers, timeout=timeout)
|
||||
if resp.status_code != 200:
|
||||
logger.info(f"ERRRRRROOOOOOORRRRR LLAMADA REST API")
|
||||
logger.info("ERRRRRROOOOOOORRRRR LLAMADA REST API")
|
||||
# return False, None, f"HTTP {resp.status_code}: {resp.text}"
|
||||
|
||||
data = resp.json()
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
from app.config import logger
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, Iterable, Optional, Tuple
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Optional, Tuple
|
||||
|
||||
|
||||
def map_tax_code(raw: str) -> str:
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
# SYNC
|
||||
ENV = development
|
||||
LOCAL_TZ = Europe/Madrid
|
||||
LAST_RUN_PATH = /usr/share/factuges-app/last_run.txt
|
||||
LAST_RUN_PATH = /usr/share/factuges-app/last_run_factuges.ini
|
||||
|
||||
FACTUGES_HOST = acana.mywire.org
|
||||
FACTUGES_PORT = 63050
|
||||
FACTUGES_DATABASE = D:\Rodax\BD\FACTUGES.FDB
|
||||
FACTUGES_USER = sysdba
|
||||
FACTUGES_PASSWORD = masterkey
|
||||
|
||||
FWEB_MYSQL_HOST = db # ${DB_NAME}
|
||||
FWEB_MYSQL_PORT = 3306 # ${DB_PORT}
|
||||
FWEB_MYSQL_DATABASE = factuges_acana # ${DB_NAME}
|
||||
FWEB_MYSQL_USER = acana # ${DB_USER}
|
||||
FWEB_MYSQL_PASSWORD = r@U8%GJ+2e/AWR # ${DB_PASS}
|
||||
|
||||
CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a'
|
||||
VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0=
|
||||
|
||||
FWEB_MYSQL_HOST = db
|
||||
FWEB_MYSQL_PORT = 3306
|
||||
FWEB_MYSQL_DATABASE = factuges_acana
|
||||
FWEB_MYSQL_USER = acana
|
||||
FWEB_MYSQL_PASSWORD = r@U8%GJ+2e/AWR
|
||||
|
||||
CTE_SERIE = 'F25/'
|
||||
CTE_STATUS_INVOICE = 'issued'
|
||||
CTE_IS_PROFORMA = 0
|
||||
@ -26,5 +26,6 @@ CTE_IS_COMPANY = 1
|
||||
CTE_SYNC_RESULT_OK = 1
|
||||
CTE_SYNC_RESULT_FAIL = 2
|
||||
|
||||
VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0=
|
||||
VERIFACTU_BASE_URL = https://api.verifacti.com/
|
||||
VERIFACTU_NIFS_API_KEY = vfn_osYpNdqSzAdTAHpazXG2anz4F3o0gfbSb5FFrCBZcno=
|
||||
@ -4,7 +4,7 @@ services:
|
||||
container_name: "factuges-sync-factuges-acana"
|
||||
restart: "no"
|
||||
environment:
|
||||
ENV: "prod"
|
||||
ENV: "production"
|
||||
LOCAL_TZ: "Europe/Madrid"
|
||||
LAST_RUN_PATH: "${LAST_RUN_PATH}"
|
||||
|
||||
@ -29,7 +29,11 @@ services:
|
||||
CTE_COUNTRY_CODE: "${CTE_COUNTRY_CODE}"
|
||||
CTE_IS_COMPANY: "${CTE_IS_COMPANY}"
|
||||
CTE_SYNC_RESULT_OK: "${CTE_SYNC_RESULT_OK}"
|
||||
CTE_SYNC_RESULT_FAIL: "${CTE_SYNC_RESULT_FAIL}"
|
||||
CTE_SYNC_RESULT_FAIL: "${CTE_SYNC_RESULT_FAIL}"
|
||||
|
||||
VERIFACTU_API_KEY: "${VERIFACTU_API_KEY}"
|
||||
VERIFACTU_BASE_URL: "${VERIFACTU_BASE_URL}"
|
||||
VERIFACTU_NIFS_API_KEY: "${VERIFACTU_NIFS_API_KEY}"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
15
setup.cfg
15
setup.cfg
@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = factuges-sync
|
||||
version = 0.0.15
|
||||
version = 0.0.21
|
||||
description = ETL job to sync data from legacy DB to MariaDB
|
||||
author = Rodax Software
|
||||
author_email = info@rodax-software.com
|
||||
@ -39,10 +39,12 @@ install_requires =
|
||||
PyNaCl==1.5.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.0.0
|
||||
requests==2.32.5
|
||||
six==1.16.0
|
||||
sshtunnel==0.4.0
|
||||
striprtf==0.0.29
|
||||
urllib3==2.2.3
|
||||
uuid6
|
||||
uuid6==2025.0.1
|
||||
|
||||
[options.extras_require]
|
||||
dev =
|
||||
@ -51,6 +53,7 @@ dev =
|
||||
packaging==24.1
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.3.6
|
||||
ruff==0.14.7
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
@ -62,9 +65,9 @@ where = .
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
exclude = .git,__pycache__,build,dist
|
||||
|
||||
[isort]
|
||||
profile = black
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py312"
|
||||
Loading…
Reference in New Issue
Block a user