revision teniendo en cuenta calculos impuestos en detalles y dto cabecera o global

This commit is contained in:
David Arranz 2025-12-03 22:49:06 +01:00
parent a34062d392
commit 6b63474cbc
6 changed files with 205 additions and 95 deletions

View File

@ -1,21 +1,24 @@
import textwrap
from datetime import date
from decimal import Decimal
from decimal import ROUND_HALF_UP, Decimal
from typing import Any, Dict, Optional
from striprtf.striprtf import rtf_to_text
from app.config import load_config
from app.config import load_config, logger
from app.utils import (
apply_discount_cents4,
cents,
cents4,
corregir_y_validar_email,
limpiar_cadena,
map_tax_code,
map_iva_code,
map_rec_by_iva_code,
money_round,
normalizar_telefono_con_plus,
normalizar_url_para_insert,
tax_fraction_from_code,
unscale_to_decimal,
)
@ -70,6 +73,25 @@ def normalize_header_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]:
"""
config = load_config()
iva_code = map_iva_code(str(fd.get("DES_TIPO_IVA")))
# Cuota de IVA de la factura
iva_frac: Optional[Decimal] = tax_fraction_from_code(iva_code) # p.ej. Decimal('0.21') o Decimal('0')
iva_percentage_value = None if (
iva_frac is None or iva_frac == 0) else cents4(iva_frac)
iva_amount = cents(fd.get('IMPORTE_IVA'))
# Cuota de RE de la factura
rec_code = None
rec_frac = None
rec_percentage_value = None
rec_amount = None
importe_re = Decimal(str(fd.get("IMPORTE_RE") or 0))
if importe_re > 0:
rec_code = map_rec_by_iva_code(iva_code)
rec_frac: Optional[Decimal] = tax_fraction_from_code(rec_code) # p.ej. Decimal('0.05') o Decimal('0.01')
rec_percentage_value = None if (rec_frac is None or rec_frac == 0) else cents4(rec_frac)
rec_amount = cents(fd.get('IMPORTE_RE'))
return {
"company_id": config['CTE_COMPANY_ID'],
"is_proforma": config["CTE_IS_PROFORMA"],
@ -90,14 +112,19 @@ def normalize_header_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]:
'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"))),
'iva_code': iva_code,
'iva_percentage_value': iva_percentage_value,
'rec_code': rec_code,
'rec_percentage_value': rec_percentage_value,
'base': cents(fd.get('BASE_IMPONIBLE')),
'iva': iva_amount,
're': rec_amount,
'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'))
'retention_code': None,
'retention_percentage_value': None,
'retention_amount_value': None,
}
@ -114,19 +141,13 @@ def normalize_details_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]:
"""
config = load_config()
iva_code = map_tax_code(str(fd.get("DES_TIPO_IVA")))
# Calcular cuota de IVA de la línea
iva_frac: Optional[Decimal] = tax_fraction_from_code(iva_code) # p.ej. Decimal('0.21') o Decimal('0')
iva_percentage_value = None if (
iva_frac is None or iva_frac == 0) else cents4(iva_frac)
rec_code = None
rec_percentage_value = None
rec_amount = None
# --- Campos base del origen ---
cantidad = fd.get("CANTIDAD")
importe_unidad = fd.get("IMPORTE_UNIDAD")
c = Decimal(str(cantidad or 0))
u = Decimal(str(importe_unidad or 0))
subtotal_amount_value = int((c * u * 10000).to_integral_value(rounding=ROUND_HALF_UP))
# total de línea (ya con descuento)
total_det = fd.get("IMPORTE_TOTAL_DET")
concepto_rtf = fd.get("CONCEPTO")
@ -137,41 +158,78 @@ def normalize_details_invoice_fields(fd: Dict[str, Any]) -> Dict[str, Any]:
unit_value = None if importe_unidad is None else cents4(
importe_unidad) # escala 4
# --- descuento % (escala 2) ---
# --- descuento de linea % (escala 2) ---
disc_raw = fd.get("DESCUENTO_DET")
disc_pct: Optional[Decimal] = None if disc_raw is None else Decimal(
str(disc_raw))
discount_percentage_value = None if (
disc_pct is None or disc_pct == 0) else cents(disc_pct)
discount_amount_value, subtotal_amount_with_dto = apply_discount_cents4(subtotal_amount_value, disc_pct)
# --- descuento global por linea % (escala 2) ---
disc_global_raw = fd.get("DESCUENTO")
disc_global_pct: Optional[Decimal] = None if disc_global_raw is None else Decimal(str(disc_global_raw))
global_percentage_value = None if (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
# --- total de línea (escala 4) ---
total_value = cents4(total_det)
# DEBE SER LO MISMO LO CALCULADO QUE LO QUE NOS VIENE POR BD DE FACTUGES
logger.info("total con dto linea calculado: %s - total que llega: %s", subtotal_amount_with_dto, total_value)
# la base imponible sobre la que calcular impuestos es el neto menos el dto de linea y dto global
base = unscale_to_decimal(taxable_amount_value, 4)
# --- cuota (escala 2) ---
# calculamos sobre el total en unidades monetarias con redondeo bancario a 2 decimales
if iva_frac is None:
iva_amount = 0
else:
base = Decimal(str(total_det or 0))
iva_amount = cents(money_round(base * iva_frac, 2))
logger.info("base imponible calculada: %s - subtotal: %s - descuentodto: %s - descuentoglobal: %s",
base, subtotal_amount_value, discount_amount_value, global_discount_amount_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
# Calcular cuota de IVA de la línea
iva_code = map_iva_code(str(fd.get("DES_TIPO_IVA")))
iva_frac: Optional[Decimal] = tax_fraction_from_code(iva_code) # p.ej. Decimal('0.21') o Decimal('0')
iva_percentage_value = None if (
iva_frac is None or iva_frac == 0) else cents4(iva_frac)
iva_amount_c4 = 0
if iva_frac:
iva_amount_c4 = cents4(money_round(base * iva_frac, 2)) # escala 4
# Cuota cuota RE de la línea
rec_code = None
rec_frac: Optional[Decimal] = None
rec_percentage_value = None
rec_amount_c4 = 0
importe_re = Decimal(str(fd.get("IMPORTE_RE") or 0))
if importe_re > 0:
rec_code = map_rec_by_iva_code(iva_code)
rec_frac = tax_fraction_from_code(rec_code) # p.ej. Decimal('0.05') o Decimal('0.01')
if rec_frac and rec_frac != 0:
rec_percentage_value = cents4(rec_frac) # escala 4 del % (0.052 -> 520)
rec_amount_c4 = cents4(base * rec_frac) # escala 4
taxes_amount_value = iva_amount_c4 + rec_amount_c4
total_value = taxable_amount_value + taxes_amount_value
return {
'position': int(fd.get("POSICION") or 0),
'description': rtf_a_texto_plano(str(concepto_rtf or "")),
'quantity_value': quantity_value,
'unit_value': unit_value,
'subtotal_amount_value': subtotal_amount_value,
'disc_pct': disc_pct,
'discount_percentage_value': discount_percentage_value,
'discount_amount_value': discount_amount_value,
'global_percentage_value': global_percentage_value,
'global_discount_amount_value': global_discount_amount_value,
'total_discount_amount_value': total_discount_amount_value,
'taxable_amount_value': taxable_amount_value,
'total_value': total_value,
'iva_code': iva_code,
'iva_percentage_value': iva_percentage_value,
'iva_amount': iva_amount,
'iva_amount_value': iva_amount_c4,
'rec_code': rec_code,
'rec_percentage_value': rec_percentage_value,
'rec_amount': rec_amount,
'rec_amount_value': rec_amount_c4,
'taxes_amount_value': taxes_amount_value,
}

View File

@ -69,23 +69,27 @@ INSERT_INVOICE_BAK = (
INSERT_INVOICE_ITEM = (
"INSERT INTO customer_invoice_items "
"(item_id, invoice_id, position, description, quantity_value, unit_amount_value, "
"discount_percentage_value, discount_amount_value, total_amount_value, "
"(item_id, invoice_id, position, description, quantity_value, unit_amount_value, subtotal_amount_value, "
"discount_percentage_value, discount_amount_value, global_percentage_value, global_discount_amount_value, total_discount_amount_value, "
" taxable_amount_value, total_amount_value, "
"iva_code, iva_percentage_value, iva_amount_value, "
"rec_code, rec_percentage_value, rec_amount_value, "
"rec_code, rec_percentage_value, rec_amount_value, taxes_amount_value, "
"quantity_scale, unit_amount_scale, discount_amount_scale, total_amount_scale, discount_percentage_scale,"
"iva_percentage_scale, iva_amount_scale, rec_percentage_scale, rec_amount_scale, retention_percentage_scale, retention_amount_scale,"
"quantity_scale, unit_amount_scale, subtotal_amount_scale, "
"discount_percentage_scale, discount_amount_scale, global_percentage_scale, global_discount_amount_scale, total_discount_amount_scale, taxable_amount_scale, total_amount_scale, "
"iva_percentage_scale, iva_amount_scale, rec_percentage_scale, rec_amount_scale, taxes_amount_scale, retention_percentage_scale, retention_amount_scale, "
"created_at, updated_at) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,2,4,2,4,2,2,4,2,4,2,4,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,2,4,4,2,4,2,4,4,4,4,2,4,2,4,4,2,4,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)"
)
INSERT_INVOICE_TAX = (
"INSERT INTO customer_invoice_taxes (tax_id, invoice_id, tax_code, taxable_amount_value, taxes_amount_value, "
"taxable_amount_scale, taxes_amount_scale, created_at, updated_at) "
"VALUES (%s,%s,%s,%s,%s,2,2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)"
"INSERT INTO customer_invoice_taxes (tax_id, invoice_id, taxable_amount_value, iva_code, iva_percentage_value, iva_amount_value, "
"rec_code, rec_percentage_value, rec_amount_value, retention_code, retention_percentage_value, retention_amount_value, taxes_amount_value, "
"taxable_amount_scale, iva_percentage_scale, iva_amount_scale, rec_percentage_scale, rec_amount_scale, retention_percentage_scale, retention_amount_scale, taxes_amount_scale, "
"created_at, updated_at) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,2,2,2,2,2,2,2,2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)"
)
# INSERT_INVOICE_ITEM_TAX = (

View File

@ -166,8 +166,6 @@ def sync_invoices_from_FACTUGES(conn_mysql, filas, conn_factuges, config):
insert_header_taxes_if_any(
cursorMySQL,
invoice_id,
factura_detalle["IVA"],
factura_detalle["RECARGO_EQUIVALENCIA"],
header_invoice_fields,
)
@ -392,48 +390,27 @@ def insert_verifactu_record(
def insert_header_taxes_if_any(
cur,
invoice_id: str,
IVA: str,
RECARGO: str,
hif: Dict[str, Any],
) -> None:
"""Inserta impuestos de cabecera"""
# 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"),
),
)
cur.execute(SQL.INSERT_INVOICE_TAX,
(
str(uuid7()),
invoice_id,
hif.get("base"),
hif.get("iva_code"),
hif.get("iva_percentage_value"),
hif.get("iva"),
hif.get("rec_code"),
hif.get("rec_percentage_value"),
hif.get("re"),
hif.get("retention_code"),
hif.get("retention_percentage_value"),
hif.get("retention_amount_value"),
hif.get("taxes_amount_value"),
),
)
def insert_item_and_taxes(cur, invoice_id: str, fields: Dict[str, Any]) -> None:
@ -452,15 +429,21 @@ def insert_item_and_taxes(cur, invoice_id: str, fields: Dict[str, Any]) -> None:
fields.get("description"),
fields.get("quantity_value"),
fields.get("unit_value"),
fields.get("subtotal_amount_value"),
fields.get("discount_percentage_value"),
None,
fields.get("discount_amount_value"),
fields.get("global_percentage_value"),
fields.get("global_discount_amount_value"),
fields.get("total_discount_amount_value"),
fields.get("taxable_amount_value"),
fields.get("total_value"),
fields.get("iva_code"),
fields.get("iva_percentage_value"),
fields.get("tax_amount"),
fields.get("iva_amount_value"),
fields.get("rec_code"),
fields.get("rec_percentage_value"),
fields.get("rec_amount"),
fields.get("rec_amount_value"),
fields.get("taxes_amount_value"),
),
)

View File

@ -1,10 +1,27 @@
from .last_execution_helper import actualizar_fecha_ultima_ejecucion, obtener_fecha_ultima_ejecucion
from .importes_helper import (
apply_discount_cents4,
cents,
cents4,
money_round,
unscale_to_decimal,
unscale_to_str,
)
from .last_execution_helper import (
actualizar_fecha_ultima_ejecucion,
obtener_fecha_ultima_ejecucion,
)
from .mails_helper import corregir_y_validar_email
from .password import hashPassword
from .send_orders_mail import send_orders_mail
from .text_converter import text_converter, limpiar_cadena
from .send_rest_api import validar_nif, estado_factura, crear_factura
from .tax_catalog_helper import TaxCatalog, get_default_tax_catalog, map_tax_code, calc_item_tax_amount, tax_fraction_from_code
from .importes_helper import unscale_to_str, unscale_to_decimal, cents, cents4, money_round
from .send_rest_api import crear_factura, estado_factura, validar_nif
from .tax_catalog_helper import (
TaxCatalog,
calc_item_tax_amount,
get_default_tax_catalog,
map_iva_code,
map_rec_by_iva_code,
tax_fraction_from_code,
)
from .telefonos_helper import normalizar_telefono_con_plus
from .mails_helper import corregir_y_validar_email
from .text_converter import limpiar_cadena, text_converter
from .websites_helper import normalizar_url_para_insert

View File

@ -1,5 +1,5 @@
from decimal import Decimal, ROUND_HALF_UP
from typing import Any, Optional
from decimal import ROUND_HALF_UP, Decimal
from typing import Any, Optional, Tuple
def cents4(value: Optional[Decimal | float | int]) -> int:
@ -53,3 +53,29 @@ def unscale_to_str(
if strip_trailing_zeros and "." in s:
s = s.rstrip("0").rstrip(".")
return s
def calc_discount_cents4(subtotal_cents4: int, disc_pct: Optional[Decimal | float | int]) -> int:
"""
Calcula el importe de descuento en escala 4 (×10000) como ENTERO.
- subtotal_cents4: subtotal ya en escala 4
- disc_pct: porcentaje de descuento (p.ej. 10 -> 10%)
Devuelve un entero NEGATIVO (como en Delphi):
ImporteDto := (-1) * ((Subtotal * Descuento) / 100);
"""
pct = Decimal(str(disc_pct or 0))
# descuento = round(subtotal * pct / 100) en la MISMA escala (×10000)
disc = (Decimal(subtotal_cents4) * pct / Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP)
return disc
def apply_discount_cents4(subtotal_cents4: int, disc_pct: Optional[Decimal | float | int]) -> Tuple[int, int]:
"""
Devuelve (discount_cents4, total_cents4) ambos en escala 4.
- discount_cents4 NEGATIVO
- total_cents4 = subtotal_cents4 + discount_cents4
"""
discount = calc_discount_cents4(subtotal_cents4, disc_pct)
total = subtotal_cents4 - discount
return discount, total

View File

@ -5,7 +5,7 @@ from pathlib import Path
from typing import Any, Dict, Iterable, Optional, Tuple
def map_tax_code(raw: str) -> str:
def map_iva_code(raw: str) -> str:
t = (raw or "").strip().lower().replace(" ", "")
mapping = {
"iva21": "iva_21",
@ -18,6 +18,18 @@ def map_tax_code(raw: str) -> str:
return mapping.get(t, "")
def map_rec_by_iva_code(raw: str) -> str:
t = (raw or "").strip().lower().replace(" ", "")
mapping = {
"iva_21": "rec_5_2",
"iva_10": "rec_1_75",
"iva_5": "rec_0_62",
"iva_4": "rec_0_5",
"iva_7_5": "rec_1",
}
return mapping.get(t, "")
def calc_item_tax_amount(tax_code: str, importe_total_det: Optional[Decimal | float | int]) -> int:
"""
Devuelve el impuesto de línea escalado a céntimos (x100) como entero.
@ -30,6 +42,11 @@ def calc_item_tax_amount(tax_code: str, importe_total_det: Optional[Decimal | fl
"iva_10": Decimal("0.10"),
"iva_4": Decimal("0.04"),
"iva_exenta": Decimal("0.00"),
"rec_5_2": Decimal('0.052'),
"rec_1_75": Decimal('0.0175'),
"rec_0_62": Decimal('0.0062'),
"rec_0_5": Decimal('0.005'),
"rec_1": Decimal('0.01')
}
rate = rates.get(tax_code)
if rate is None:
@ -55,8 +72,13 @@ def tax_fraction_from_code(tax_code: str) -> Decimal:
'iva_2': Decimal('0.02'),
'iva_0': Decimal('0.00'),
'iva_exenta': Decimal('0.00'),
'rec_5_2': Decimal('0.052'),
'rec_1_75': Decimal('0.0175'),
'rec_0_62': Decimal('0.0062'),
'rec_0_5': Decimal('0.005'),
'rec_1': Decimal('0.01')
}
return mapping.get(tax_code or '', Decimal('0.13'))
return mapping.get(tax_code or '', Decimal('0.00'))
def reduce_scale_pair(value: int, scale: int, *, min_scale: int = 0) -> Tuple[int, int]: