This commit is contained in:
David Arranz 2025-11-20 19:51:03 +01:00
parent 4a8188d255
commit 9a34e38557
9 changed files with 261 additions and 241 deletions

View File

@ -30,12 +30,6 @@ DEV_UECKO_MYSQL_DATABASE = uecko_erp_sync
DEV_UECKO_MYSQL_USER = rodax DEV_UECKO_MYSQL_USER = rodax
DEV_UECKO_MYSQL_PASSWORD = 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_API_KEY = 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
BREVO_EMAIL_TEMPLATE = 1 BREVO_EMAIL_TEMPLATE = 1

View File

@ -35,12 +35,6 @@ UECKO_MYSQL_DATABASE = uecko
UECKO_MYSQL_USER = uecko UECKO_MYSQL_USER = uecko
UECKO_MYSQL_PASSWORD = u8Ax5Nw3%sjd 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_API_KEY = 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
BREVO_EMAIL_TEMPLATE = 1 BREVO_EMAIL_TEMPLATE = 1

View File

@ -24,27 +24,23 @@ def load_config():
'UECKO_MYSQL_USER': os.getenv('UECKO_MYSQL_USER'), 'UECKO_MYSQL_USER': os.getenv('UECKO_MYSQL_USER'),
'UECKO_MYSQL_PASSWORD': os.getenv('UECKO_MYSQL_PASSWORD'), 'UECKO_MYSQL_PASSWORD': os.getenv('UECKO_MYSQL_PASSWORD'),
'FACTUGES_ID_EMPRESA': os.getenv('FACTUGES_ID_EMPRESA'), 'CTE_COMPANY_ID': os.getenv('CTE_COMPANY_ID'),
'FACTUGES_PRECIO_PUNTO': os.getenv('FACTUGES_PRECIO_PUNTO'), 'CTE_SERIE': os.getenv('CTE_SERIE'),
'FACTUGES_NOMBRE_TARIFA': os.getenv('FACTUGES_NOMBRE_TARIFA'), 'CTE_STATUS_INVOICE': os.getenv('CTE_STATUS_INVOICE'),
'FACTUGES_CONTRATO_ID_TIENDA': os.getenv('FACTUGES_CONTRATO_ID_TIENDA'), 'CTE_IS_PROFORMA': os.getenv('CTE_IS_PROFORMA'),
'FACTUGES_CONTRATO_SITUACION': os.getenv('FACTUGES_CONTRATO_SITUACION'), 'CTE_STATUS_VERIFACTU': os.getenv('CTE_STATUS_VERIFACTU'),
'FACTUGES_CONTRATO_ENVIADA_REVISADA': os.getenv('FACTUGES_CONTRATO_ENVIADA_REVISADA'), 'CTE_LANGUAGE_CODE': os.getenv('CTE_LANGUAGE_CODE'),
'FACTUGES_CONTRATO_TIPO_DETALLE': os.getenv('FACTUGES_CONTRATO_TIPO_DETALLE'), '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_BASE_URL': os.getenv('VERIFACTU_BASE_URL'),
'VERIFACTU_API_KEY': os.getenv('VERIFACTU_API_KEY'), 'VERIFACTU_API_KEY': os.getenv('VERIFACTU_API_KEY'),
'VERIFACTU_NIFS_API_KEY': os.getenv('VERIFACTU_NIFS_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'),
} }

121
app/db/normalizations.py Normal file
View File

@ -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
}

View File

@ -1,36 +1,37 @@
# ========================= # =========================
# MYSQL (constantes) # 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_CUSTOMER_BY_FACTUGES = (
"SELECT customers.id FROM customers WHERE customers.factuges_id=%s" "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_METHOD_BY_FACTUGES = (
"SELECT payment_methods.id FROM payment_methods WHERE payment_methods.factuges_id=%s" "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_PAYMENT_METHOD = (
"INSERT INTO payment_methods (id, description, factuges_id, created_at, updated_at) " "INSERT INTO payment_methods (id, description, factuges_id, created_at, updated_at) "
"VALUES (%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)" "VALUES (%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)"
) )
INSERT_INVOICE = ( 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, " "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, " "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, " "payment_method_id, payment_method_description, factuges_id, "
@ -40,16 +41,22 @@ INSERT_INVOICE = (
"%s AS id, " "%s AS id, "
"%s AS company_id, " "%s AS company_id, "
"COALESCE(MAX(invoice_number + 0),0)+1 AS invoice_number, " "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 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 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, " "%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 " "2,2,2,2,2,2, 'es','EUR', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP "
"FROM customer_invoices " "FROM customer_invoices "
"WHERE company_id = %s " "WHERE company_id = %s "
"AND is_proforma = %s "
"AND deleted_at is null" "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_INVOICE_BAK = (
"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, 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, " "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)" "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_INVOICE_ITEM = (
"INSERT INTO customer_invoice_items " "INSERT INTO customer_invoice_items "
"(item_id, invoice_id, position, description, quantity_value, unit_amount_value, " "(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)" "VALUES (%s,%s,%s,%s,%s,4,2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)"
) )
SELECT_INVOICES_DELETED = ( UPDATE_CUSTOMER = (
"SELECT ci.id " "UPDATE customers SET name=%s, tin=%s, street=%s, city=%s, province=%s, postal_code=%s, country=%s, language_code=%s, is_company=%s, "
"FROM customer_invoices as ci " "phone_primary=%s, phone_secondary=%s, mobile_primary=%s, mobile_secondary=%s, "
"WHERE " "email_primary=%s, email_secondary=%s, website=%s, updated_at=CURRENT_TIMESTAMP "
"(ci.deleted_at is not null) " "WHERE (id=%s)"
) )
# ========================= # =========================
# FIREBIRD (constantes) # FIREBIRD (constantes)
# ========================= # =========================
SELECT_FACTUGES_FACTURAS_CLIENTE = ( 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"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.CALLE, fac.POBLACION, fac.PROVINCIA, fac.CODIGO_POSTAL, fac.FECHA_ALTA, "
@ -124,3 +131,47 @@ LIMPIAR_FACTUGES_LINK = (
"VERIFACTU = 0 " "VERIFACTU = 0 "
"WHERE (ID_VERIFACTU = ?)" "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 "
)

View File

@ -1,18 +1,9 @@
import logging import logging
import textwrap from typing import Dict, Any
from typing import Dict, Any, Optional, Tuple
from uuid6 import uuid7 from uuid6 import uuid7
from config import load_config from config import load_config
from decimal import Decimal, ROUND_HALF_UP
from . import sql_sentences as SQL 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 . import normalizations as NORMALIZA
from striprtf.striprtf import rtf_to_text
# =========================
# constantes
# =========================
# Compañia RODAX
cte_company_id = '5e4dc5b3-96b9-4968-9490-14bd032fec5f'
def sync_invoices(conn_factuges, conn_mysql, last_execution_date): 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: if ids_verifactu_deleted:
sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config) sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config)
else: 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: except Exception as e:
if cursor_mysql is not None: if cursor_mysql is not None:
@ -85,7 +77,7 @@ def sync_delete_invoices(conn_factuges, ids_verifactu_deleted, config):
try: try:
cursor_FactuGES = conn_factuges.cursor() cursor_FactuGES = conn_factuges.cursor()
if ids_verifactu_deleted: 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, [( cursor_FactuGES.executemany(SQL.LIMPIAR_FACTUGES_LINK, [(
id_verifactu,) for id_verifactu in ids_verifactu_deleted]) id_verifactu,) for id_verifactu in ids_verifactu_deleted])
else: else:
@ -118,13 +110,12 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
for factura_detalle in filas: for factura_detalle in filas:
# Preparamos los campos para evitar errores # Preparamos los campos para evitar errores
customer_fields = normalize_customer_fields(factura_detalle) customer_fields = NORMALIZA.normalize_customer_fields(
header_invoice_fields = normalize_header_invoice_fields(
factura_detalle) 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) factura_detalle)
logging.info(
f"FACTURAS_CLIENTE DETALLLLLLLLLLESSSSSSS: {details_invoice_fields}")
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: 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 # Comprobamos si existe el cliente del primer item de la factura
customer_id = get_or_create_customer( customer_id = get_or_create_customer(
cursorMySQL, cursorMySQL,
cte_company_id, config['CTE_COMPANY_ID'],
str(factura_detalle["ID_CLIENTE"]), str(factura_detalle["ID_CLIENTE"]),
customer_fields, 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 # 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 # ---- cabecera factura
invoice_id = insert_invoice_header( invoice_id = insert_invoice_header(cursorMySQL, customer_fields, header_invoice_fields, customer_id, pm_id, str(
cursorMySQL, cte_company_id, customer_fields, header_invoice_fields, customer_id, pm_id, str( factura_detalle["DES_FORMA_PAGO"]), config
factura_detalle["DES_FORMA_PAGO"])
) )
# ---- impuestos cabecera # ---- impuestos cabecera
insert_header_taxes_if_any( 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)
# Guardamos en Factuges el id de la customer_invoice # Guardamos en Factuges el id de la customer_invoice
logging.info( logging.info(
f"Updating FACTURAS_CLIENTE {invoice_id} {factuges_id}") 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() 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: 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 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, SQL.INSERT_CUSTOMER,
( (
customer_id, fields["name"], fields["tin"], fields["street"], fields["city"], fields["province"], 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["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 return customer_id
@ -316,7 +207,7 @@ def get_or_create_customer(cur, company_id: str, factuges_customer_id: str, fiel
SQL.UPDATE_CUSTOMER, SQL.UPDATE_CUSTOMER,
( (
fields["name"], fields["tin"], fields["street"], fields["city"], fields["province"], fields["postal_code"], 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["mobile_primary"], fields["mobile_secondary"], fields["email_primary"], fields["email_secondary"],
fields["website"], customer_id, fields["website"], customer_id,
), ),
@ -342,19 +233,19 @@ def get_or_create_payment_method(cur, factuges_payment_id: str, description: str
return pm_id return pm_id
def insert_invoice_header(cur, company_id: str, cf: Dict[str, Any], hif: Dict[str, Any], customer_id: 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) -> str: payment_method_id: str, payment_method_description: str, config) -> str:
""" """
Inserta cabecera y devuelve invoice_id Inserta cabecera y devuelve invoice_id
""" """
invoice_id = str(uuid7()) invoice_id = str(uuid7())
logging.info("Inserting invoice %s %s %s", logging.info("Inserting invoice %s %s %s %s",
invoice_id, hif.get('reference'), hif.get('invoice_date')) invoice_id, hif.get('reference'), hif.get('invoice_date'), config['CTE_STATUS_INVOICE'])
cur.execute( cur.execute(
SQL.INSERT_INVOICE, 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'), 'invoice_date'), hif.get('operation_date'), hif.get('description'),
hif.get('subtotal_amount_value'), hif.get('discount_amount_value'), hif.get( hif.get('subtotal_amount_value'), hif.get('discount_amount_value'), hif.get(
'discount_percentage_val'), hif.get('taxable_amount_value'), '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'), 'name'), cf.get('street'), cf.get('city'),
cf.get('province'), cf.get('postal_code'), 'es', cf.get('province'), cf.get('postal_code'), 'es',
payment_method_id, payment_method_description, hif.get( 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 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: def insert_header_taxes_if_any(cur, invoice_id: str, IVA: str, RECARGO: str, hif: Dict[str, Any]) -> None:
""" """
Inserta impuestos de cabecera Inserta impuestos de cabecera

View File

@ -4,49 +4,18 @@ from typing import Dict, Any, Tuple, Optional, List, Iterable
from config import load_config from config import load_config
from decimal import Decimal from decimal import Decimal
from utils import validar_nif, estado_factura, crear_factura, TaxCatalog, unscale_to_str 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): def sync_invoices_verifactu(conn_mysql, last_execution_date):
config = load_config() 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 # Crear un cursor para ejecutar consultas SQL
cursor_mysql = None cursor_mysql = None
try: try:
cursor_mysql = conn_mysql.cursor() cursor_mysql = conn_mysql.cursor()
# Ejecutar la consulta de FACTURAS_CLIENTE # Ejecutar la consulta de customer invoices a enviar
cursor_mysql.execute(consulta_sql_customer_invoices_issue) cursor_mysql.execute(SQL.consulta_sql_customer_invoices_issue)
filas = cursor_mysql.fetchall() filas = cursor_mysql.fetchall()
# Obtener los nombres de las columnas # 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 # Verificar si hay filas en el resultado
if tuplas_seleccionadas: if tuplas_seleccionadas:
enviar_datos(tuplas_seleccionadas, cursor_mysql, config) enviar_datos(tuplas_seleccionadas, cursor_mysql, config)
logging.info(f"Ha ido bien enviar_datos") logging.info(f"Ok send Verifactu")
else: else:
logging.info(f"There are no rows to send") 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 invoice_id = None
factura = None factura = None
for fila in invoices_to_verifactu: 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 # y creamos la cabecera de la factura siguiente, si no existe factura solo la creamos
if invoice_id != str(fila['id']): if invoice_id != str(fila['id']):
@ -132,26 +101,15 @@ def procesar_factura_verifactu(
config: Dict[str, Any] config: Dict[str, Any]
) -> bool: ) -> 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: if factura != None:
# Creamos registro de factura en verifactu # Creamos registro de factura en verifactu
if factura.get('uuid') is None: if factura.get('uuid') == '':
# logging.info(f"Send to create Verifactu: {factura}") # logging.info(f"Send to create Verifactu: {factura}")
respuesta = crear_factura(factura, config) respuesta = crear_factura(factura, config)
if respuesta.get("status") == 200 and respuesta.get("ok"): if respuesta.get("status") == 200 and respuesta.get("ok"):
data = respuesta.get("data") data = respuesta.get("data")
cursor_mysql.execute(insert_verifactu_records, (str(uuid4()), factura.get( cursor_mysql.execute(SQL.update_verifactu_records_with_invoiceId, (data.get("estado"), data.get(
"id"), data.get("estado"), data.get("uuid"), data.get("url"), data.get("qr"))) "uuid"), data.get("url"), data.get("qr"), factura.get("id")))
logging.info( logging.info(
f">>> Factura {factura.get("reference")} registrada en Verifactu") f">>> Factura {factura.get("reference")} registrada en Verifactu")
return True return True
@ -165,7 +123,7 @@ def procesar_factura_verifactu(
respuesta = estado_factura(factura.get('uuid'), config) respuesta = estado_factura(factura.get('uuid'), config)
if respuesta.get("status") == 200 and respuesta.get("ok"): if respuesta.get("status") == 200 and respuesta.get("ok"):
data = respuesta.get("data") 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'))) 'estado'), data.get('operacion'), factura.get('uuid')))
logging.info( logging.info(
f">>> Factura {factura.get("reference")} actualizado registro de Verifactu") f">>> Factura {factura.get("reference")} actualizado registro de Verifactu")

View File

@ -60,7 +60,7 @@ def main():
# Sync Verifactu # Sync Verifactu
logging.info( logging.info(
f">>>>>>>>>> Sync facturas emitidas en FactuGES web to Verifactu") 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.commit()
conn_mysql.close() conn_mysql.close()
logging.info(f"FIN Sync Verifactu >>>>>>>>>>") logging.info(f"FIN Sync Verifactu >>>>>>>>>>")

View File

@ -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. Devuelve la fracción (0.21, 0.18, ...) según el tax_code normalizado.
Exenta -> 0. Exenta -> 0.
""" """
logging.info(f"FACTURAS_CLIENTE IVAAAAAAAAAAAAAAA {tax_code}")
# Si ya tienes TaxCatalog, puedes delegarlo allí; lo dejo inline para simplicidad. # Si ya tienes TaxCatalog, puedes delegarlo allí; lo dejo inline para simplicidad.
mapping = { mapping = {
'iva_21': Decimal('0.21'), 'iva_21': Decimal('0.21'),