"""E8 · banco sintético para las DOS implementaciones (dentro del contenedor: /p = e8_precios, /ref = crudos, /a1 = A1). Base inventada con contratos plantados (razón conocida, calculada con la referencia real) y casos que deben caer fuera; se corren c2_calculo.py (impl1) e impl2_calculo.py (que compara con la 1) y se comprueba: estados y razones esperados, 0 diferencias entre implementaciones, hechos por órgano en los plantados y en ningún otro; y un segundo banco con CERO válidos.""" import sys, os, csv, json, subprocess, datetime as dt sys.path.insert(0, '/p/scripts'); sys.path.insert(0, '/a1/scripts') import pyarrow as pa, pyarrow.parquet as pq import reglas_e8 as R REF = '/p/salidas' rc = {(r['mes'], r['prov_ine'], r['producto']): float(r['ref_sin_iva_eur_l']) for r in csv.DictReader(open(f'{REF}/ref_carb_mensual.csv'))} re_ = {(r['semestre'], r['banda']): float(r['eur_kwh']) for r in csv.DictReader(open(f'{REF}/ref_electricidad_eurostat.csv')) if r['impuestos'] == 'X_VAT'} filas = []; esperado = {} def fila(id_, mercado, objeto, imp, fecha, organo, esp_estado, esp_razon=None, **kw): r = dict(source='BANCO', id_canonical=id_, licitacion_id=None, lote_id=None, menor=True, anio_ref=fecha.year if fecha else 2013, cpv=None, objeto=objeto, imp=imp, imp_adj_eur=imp, iva_base='sin', rol_am=None, nuts='ES300', provincia='Madrid', ccaa=None, organo_nif='P2800000A', organo_nombre=f'Órgano {organo}', dir3=None, fecha_adj=fecha, fecha_formalizacion=None, fecha_pub=None, duracion_meses=12.0, procedimiento=None, num_ofertas=None, url_origen=None, copia_de=None, correccion_imp_adj=None, cuenta=True, organo_clave=f'ORG_{organo}', tipo_organo='ayuntamiento', mercado=mercado, via_mercado='texto') r.update(kw); filas.append(r); esperado[(id_, mercado)] = (esp_estado, esp_razon) meses = [dt.date(2018, m, 15) for m in range(1, 13)] for i, f in enumerate(meses): # plantado: gasóleo C a 1,30 × referencia fila(f'c_p{i}', 'carburantes', 'Suministro de 4.000 litros de gasóleo C para calefacción del colegio', 4000 * rc[(f'{f:%Y-%m}', '00', 'gasoleo_c')] * 1.30, f, 'PLANT_C', 'valido', 1.30) for j in range(4): # normales a 1,00 (gasóleo C) en 4 órganos for i, f in enumerate(meses[:10]): fila(f'c_n{j}_{i}', 'carburantes', 'Suministro de 2.500 litros de gasóleo C para calefacción', 2500 * rc[(f'{f:%Y-%m}', '00', 'gasoleo_c')] * (0.97 + 0.006 * i), f, f'N{j}', 'valido', 0.97 + 0.006 * i) fila('c_a_prov', 'carburantes', 'Suministro de 3.000 litros de gasóleo A para vehículos', 3000 * rc[('2023-05', '28', 'gasoleo_a')] * 0.9, dt.date(2023, 5, 10), 'X1', 'valido', 0.9) f = dt.date(2018, 5, 10) fila('c_copia', 'carburantes', 'Suministro de 3.000 litros de gasóleo C', 3000.0, f, 'X1', 'copia', copia_de='X') fila('c_cuenta', 'carburantes', 'Suministro de 3.000 litros de gasóleo C', 3000.0, f, 'X1', 'copia', cuenta=False) fila('c_sinimp', 'carburantes', 'Suministro de 3.000 litros de gasóleo C', None, f, 'X1', 'sin_importe') fila('c_am', 'carburantes', 'Acuerdo marco de 3.000 litros de gasóleo C', 3000.0, f, 'X1', 'acuerdo_marco', rol_am='acuerdo') fila('c_canarias', 'carburantes', 'Suministro de 3.000 litros de gasóleo C', 3000.0, f, 'X1', 'fuera_territorio', nuts='ES705', provincia='Las Palmas') fila('c_dudoso', 'carburantes', 'Suministro de 3.000 litros de gasóleo C para calefacción', 3000 * rc[('2018-05', '00', 'gasoleo_c')] * 3.0, f, 'X1', 'dudoso', 3.0) fila('c_sincant', 'carburantes', 'Suministro de gasóleo C para calefacción', 3000.0, f, 'X1', 'sin_cantidad') fila('c_g98', 'carburantes', 'Suministro de 3.000 litros de gasolina sin plomo 98', 3000.0, f, 'X1', 'sin_producto_comparable') fila('c_nucleo', 'carburantes', 'Suministro de 150 litros de gasóleo C para calefacción', 150 * rc[('2018-05', '00', 'gasoleo_c')], f, 'X1', 'fuera_nucleo', 1.0) fila('c_periodo', 'carburantes', 'Suministro de 3.000 litros de gasóleo C', 3000.0, None, 'X1', 'fuera_periodo') for i in range(11): # papel plantado a 1,5 × mediana AM 20/2023 lote 1 80 g (17,875) fp = dt.date(2024, 6, 1) + dt.timedelta(days=30 * i) fila(f'p_p{i}', 'papel', 'Suministro de 20 cajas de papel A4 80 g fibra virgen', 20 * 17.875 * 1.5, fp, 'PLANT_P', 'valido', 1.5) for j in range(3): for i in range(10): fp = dt.date(2024, 6, 1) + dt.timedelta(days=30 * i) fila(f'p_n{j}_{i}', 'papel', 'Suministro de 100 paquetes de papel reciclado DIN A4', 20 * 15.95 * 1.0, fp, f'PN{j}', 'valido', 1.0) fila('p_hueco', 'papel', 'Suministro de 20 cajas de papel A4 fibra virgen', 300.0, dt.date(2023, 1, 10), 'X2', 'sin_referencia') fila('p_grupo', 'papel', 'Contrato basado en el Acuerdo Marco 20/2023 para la adquisición de 10 cajas de papel reciclado A4 80 g', 159.5, dt.date(2024, 9, 1), 'DGRCC', 'valido', 1.0, rol_am='basado') fila('p_a3', 'papel', 'Suministro de 20 cajas de papel A3', 300.0, dt.date(2024, 9, 1), 'X2', 'formato_no_a4') fila('p_pequeno', 'papel', 'Suministro de 2 paquetes de papel A4', 9.0, dt.date(2024, 9, 1), 'X2', 'fuera_nucleo', 9.0 / 0.4 / 17.875) re24 = re_[('2024-S1', 'MWH20-499')] for i in range(3): fila(f'e_{i}', 'electricidad', 'Suministro de energía eléctrica con un consumo estimado de 300.000 kWh/año', 300000 * re24, dt.date(2024, 3, 1), f'E{i}', 'valido', 1.0) fila('e_umbral', 'electricidad', 'Suministro de energía eléctrica para consumos inferiores a 1.000.000 kWh', 1000.0, dt.date(2024, 3, 1), 'E9', 'sin_cantidad') fila('e_s2_2026', 'electricidad', 'Suministro de energía eléctrica, consumo de 50.000 kWh', 1000.0, dt.date(2026, 9, 1), 'E9', 'fuera_periodo') def con_extraccion(rows): for r in rows: o, m = r['objeto'], r['mercado'] if m == 'carburantes': c = R.cantidad_carb(o) r.update(x_motivo='ok' if c else 'sin_cantidad', x_cantidad=c[0] if c else None, x_unidad='litros', x_literal=c[1] if c else None, x_n_distintas=c[2] if c else 0, x_producto=R.producto_carb(o), x_anual=None, x_gramaje=None, x_formato_explicito=None, x_excl_v2=R.excluido_v2(o, 'carburantes')) elif m == 'electricidad': e = R.energia_elec(o) r.update(x_motivo=e['motivo'], x_cantidad=e.get('kwh'), x_unidad='kwh', x_literal=e.get('literal'), x_n_distintas=e.get('n_distintas', 0), x_producto='electricidad', x_anual=e.get('anual'), x_gramaje=None, x_formato_explicito=None, x_excl_v2=False) else: e = R.papel(o) r.update(x_motivo=e['motivo'], x_cantidad=e.get('cajas_eq'), x_unidad='caja_2500', x_literal=e.get('literal'), x_n_distintas=1 if e['motivo'] == 'ok' else 0, x_producto=e.get('fibra'), x_anual=None, x_gramaje=e.get('gramaje'), x_formato_explicito=e.get('formato_explicito'), x_excl_v2=False) return rows def correr(rows, carpeta): os.makedirs(f'{carpeta}/priv', exist_ok=True); os.makedirs(f'{carpeta}/salidas', exist_ok=True) pq.write_table(pa.Table.from_pylist(con_extraccion(rows)), f'{carpeta}/base.parquet') a = subprocess.run([sys.executable, '-I', '/p/scripts/c2_calculo.py', f'{carpeta}/base.parquet', carpeta, REF], capture_output=True, text=True) b = subprocess.run([sys.executable, '-I', '/p/scripts/impl2_calculo.py', f'{carpeta}/base.parquet', carpeta, '/ref', f'{carpeta}/priv/e8_contratos_impl1.csv', f'{carpeta}/salidas/e8_organos_impl1.csv', f'{carpeta}/salidas/e8_impl2.json'], capture_output=True, text=True) return a, b fallos = 0 a, b = correr(filas, '/p/tmp/banco/uno') if a.returncode or b.returncode: print('ERROR', a.stderr[-2000:], b.stderr[-2000:]); sys.exit(1) I1 = {(r['id_canonical'], r['mercado']): r for r in csv.DictReader(open('/p/tmp/banco/uno/priv/e8_contratos_impl1.csv'))} for k, (est, raz) in esperado.items(): r = I1[k] if r['estado'] != est or (raz is not None and abs(float(r['razon']) - raz) > 1e-9): fallos += 1; print('FALLO esperado', k, r['estado'], r['razon'], '≠', est, raz) J = json.load(open('/p/tmp/banco/uno/salidas/e8_impl2.json')) for m in ('carburantes', 'electricidad', 'papel'): if J[m]['diferencias'].get('contratos', 0) or J[m]['diferencias'].get('falta', 0): fallos += 1; print('FALLO impl1≠impl2', m, J[m]['diferencias'], J[m]['ejemplos'][:3]) O = list(csv.DictReader(open('/p/tmp/banco/uno/salidas/e8_organos_impl1.csv'))) hechos = {(o['organo_clave'], o['producto']) for o in O if o['hecho'] != 'ninguno'} if hechos != {('ORG_PLANT_C', 'gasoleo_c'), ('ORG_PLANT_P', 'todos')}: fallos += 1; print('FALLO hechos', hechos) if J['organos']['pct_iguales'] != 100.0: fallos += 1; print('FALLO órganos impl2', J['organos']) res = json.load(open('/p/tmp/banco/uno/salidas/e8_resumen_impl1.json')) if res['papel']['grupo_referencia']['n'] != 1: fallos += 1; print('FALLO grupo de referencia', res['papel']['grupo_referencia']) # banco 2: cero válidos cero = [r for r in filas if esperado[(r['id_canonical'], r['mercado'])][0] != 'valido'] esperado_cero = len(cero) a, b = correr([dict(r) for r in cero], '/p/tmp/banco/cero') if a.returncode or b.returncode: fallos += 1; print('FALLO banco cero', a.stderr[-1500:], b.stderr[-1500:]) else: res0 = json.load(open('/p/tmp/banco/cero/salidas/e8_resumen_impl1.json')) if any(res0[m]['validos'] for m in res0): fallos += 1; print('FALLO cero válidos', {m: res0[m]['validos'] for m in res0}) print('banco: contratos', len(filas), 'cero', esperado_cero, 'fallos', fallos) sys.exit(1 if fallos else 0)