""" actividad.py — El rendimiento de una actividad económica (la minería), año a año: lo que va a la parte GENERAL de la declaración, no a la del ahorro. Minar es una actividad económica (DGT V3625-16, V1748-18): lo minado es su ingreso, por lo que valía el día que se recibió, y de él se restan los gastos de la actividad. Se cuenta con las operaciones que llevan la etiqueta de la actividad (la misma que usa actividades.py para la rentabilidad): ingresos lo minado o recibido en el año, a su valor de ese día gastos los gastos corrientes del año (hosting, luz, reparaciones), menos lo que te hayan devuelto de ellos amortización los aparatos no se restan de golpe: cada año, un porcentaje de lo que costaron (el primero, en proporción a los días), hasta agotarlo. Un gasto es un aparato si lleva la marca «amortizable»; lo devuelto de un aparato baja su precio. reducción en estimación directa simplificada, un porcentaje del rendimiento cuando es positivo (con tope, donde lo haya) rendimiento ingresos − gastos − amortización − reducción En el País Vasco la modalidad simplificada no amortiza aparte («amortiza»: false): la reducción cubre amortizaciones y gastos de difícil justificación, y desde 2025 o 2026, según el territorio, va por tramos («reduccion_tramos»): o por partes de la diferencia, como un IRPF en pequeño («por_partes»), o con un solo porcentaje según la diferencia del año anterior («año_anterior»). Las reglas que cambian con los años van en «ejercicios», como en cuota.py. Si sale negativo, resta del resto de la parte general de ese año. Aquí no se calcula la cuota: la parte general depende de lo demás que ganes (tu trabajo), que la cartera no conoce. Los porcentajes están en reglas/.json → «actividad», con su cita. """ from __future__ import annotations import datetime as dt from ..actividades import clave def _dias_del_año(año: int) -> int: return (dt.date(año + 1, 1, 1) - dt.date(año, 1, 1)).days def _aparatos(gastos: list[dict]) -> list[dict]: """Los aparatos de una actividad con su precio final: lo devuelto de un aparato se descuenta del más antiguo que aún tenga precio.""" aparatos = [{"movimiento": g["movimiento"], "fecha": g["fecha"], "notas": g["notas"], "coste": g["importe"], "estimado": g["estimado"]} for g in gastos if g["importe"] > 0] for g in gastos: falta = -g["importe"] for a in aparatos: if falta <= 0.004: break q = min(a["coste"], falta) a["coste"] = round(a["coste"] - q, 2) a["estimado"] = a["estimado"] or g["estimado"] falta -= q return [a for a in aparatos if a["coste"] > 0.004] def amortizar(aparato: dict, ejercicio: int, coeficiente: float) -> dict: """Lo amortizado antes de ese ejercicio y en él, por tablas y en línea recta: coste × coeficiente cada año, el primero en proporción a los días desde que se compró, hasta agotar el coste.""" compra = dt.date.fromisoformat(aparato["fecha"]) antes = este = 0.0 for año in range(compra.year, ejercicio + 1): cuota = aparato["coste"] * coeficiente / 100 if año == compra.year: cuota *= ((dt.date(año, 12, 31) - compra).days + 1) / _dias_del_año(año) cuota = round(min(cuota, aparato["coste"] - antes), 2) if año < ejercicio: antes = round(antes + cuota, 2) else: este = cuota return {**aparato, "antes": antes, "amortizacion": este, "pendiente": round(aparato["coste"] - antes - este, 2)} def del_ejercicio(reglas: dict, ejercicio: int) -> dict: """Las reglas de la actividad que rigen ese año: las generales con lo que cambie el primer tramo de «ejercicios» cuyo «desde» no lo supere.""" base = {k: v for k, v in reglas.items() if k != "ejercicios"} for tramo in reglas.get("ejercicios", []): if ejercicio >= tramo["desde"]: return {**base, **tramo, "citas": {**base.get("citas", {}), **tramo.get("citas", {})}} return base def _eur(v: float) -> str: return f"{v:,.0f}".replace(",", ".") + " €" def reduccion(previo: float, anterior: float | None, reglas: dict) -> tuple[float, float, str]: """(reducción, porcentaje efectivo, cómo se ha calculado) de la estimación directa simplificada sobre un rendimiento previo positivo. «anterior» es la diferencia entre ingresos y gastos del año anterior, si lo hubo.""" if previo <= 0: return 0.0, 0.0, "" tramos = reglas.get("reduccion_tramos") if not tramos: pct = reglas["reduccion_simplificada"] tope = reglas.get("reduccion_maxima") importe = round(min(previo * pct, tope or previo), 2) texto = f"Reducción del {pct:.0%}" + (f", con un máximo de {_eur(tope)}" if tope else "") return importe, pct, texto if reglas.get("reduccion_modo") == "año_anterior": mira = anterior if anterior is not None else previo pct = next(p for hasta, p in tramos if hasta is None or mira <= hasta) cual = "del año anterior" if anterior is not None else "de este año (no hay año anterior)" return round(previo * pct, 2), pct, f"Reducción del {pct:.0%}, por la diferencia entre ingresos y gastos {cual}" importe, desde = 0.0, 0.0 for hasta, p in tramos: if previo <= desde: break importe += ((previo if hasta is None else min(previo, hasta)) - desde) * p if hasta is None: break desde = hasta importe = round(importe, 2) partes = ", ".join(f"{p:.0%} hasta {_eur(h)}" if h else f"{p:.0%} del resto" for h, p in tramos) return importe, round(importe / previo, 4), f"Reducción por partes ({partes})" def rendimientos(gastos: list[dict], recibido: list[dict], ejercicio: int, opciones: dict, reglas: dict) -> list[dict]: """Una ficha por actividad con algo que contar ese ejercicio. gastos y recibido son los de TODOS los años (los que apunta el motor del FIFO): un aparato de hace tres años se sigue amortizando este.""" reglas = del_ejercicio(reglas, ejercicio) amortiza = reglas.get("amortiza", True) tablas = reglas.get("amortizacion") or {} tabla = (tablas.get(str(opciones.get("amortizacion"))) or next(iter(tablas.values()))) if amortiza else None año = str(ejercicio) nombres: dict[str, str] = {} for x in gastos + recibido: if x.get("actividad"): nombres.setdefault(clave(x["actividad"]), x["actividad"]) fichas = [] for k, nombre in nombres.items(): suyos = [g for g in gastos if clave(g["actividad"]) == k] entradas = [r for r in recibido if clave(r["actividad"]) == k and r["fecha"].startswith(año)] corrientes = [g for g in suyos if not g["amortizable"] and g["fecha"].startswith(año)] comprados = _aparatos([g for g in suyos if g["amortizable"] and g["fecha"] <= f"{año}-12-31"]) if amortiza: aparatos = [amortizar(a, ejercicio, tabla["coeficiente"]) for a in comprados] aparatos = [a for a in aparatos if a["amortizacion"] or a["pendiente"] or a["fecha"].startswith(año)] else: aparatos = [] if not (entradas or corrientes or any(a["amortizacion"] for a in aparatos)): continue ingresos = round(sum(r["valor"] for r in entradas), 2) gasto = round(sum(g["importe"] for g in corrientes), 2) amortizacion = round(sum(a["amortizacion"] for a in aparatos), 2) previo = round(ingresos - gasto - amortizacion, 2) # La diferencia del año anterior, para los territorios que eligen el porcentaje por ella antes = str(ejercicio - 1) entradas_antes = [r for r in recibido if clave(r["actividad"]) == k and r["fecha"].startswith(antes)] gastos_antes = [g for g in suyos if not g["amortizable"] and g["fecha"].startswith(antes)] anterior = round(sum(r["valor"] for r in entradas_antes) - sum(g["importe"] for g in gastos_antes), 2) \ if entradas_antes or gastos_antes else None rebaja, porcentaje, texto_reduccion = reduccion(previo, anterior, reglas) fichas.append({ "nombre": nombre, "clave": k, "ejercicio": ejercicio, "ingresos": ingresos, "entradas": len(entradas), "cantidad": sum(r["cantidad"] for r in entradas), "gastos": gasto, "corrientes": sorted(corrientes, key=lambda g: g["fecha"]), "amortizacion": amortizacion, "aparatos": aparatos, "tabla": tabla, "previo": previo, "reduccion": rebaja, "porcentaje_reduccion": porcentaje, "texto_reduccion": texto_reduccion, "amortiza": amortiza, "aparatos_sin_amortizar": 0 if amortiza else len(comprados), "rendimiento": round(previo - rebaja, 2), "estimados": sum(1 for x in corrientes + aparatos if x["estimado"]), "verificado": reglas["verificado"], "nota": reglas["nota"], "citas": reglas["citas"], "texto_negativo": reglas.get("texto_negativo"), }) return sorted(fichas, key=lambda f: -abs(f["rendimiento"]))