feat(monday): obra, atrasos, hitos, saldos y flujo de escrituras desde Reporte_EstatusVenta

- Script sync-obra-estatus.py (idempotente, --dry-run, retry/backoff):
  parsea ESTATUS OBRA, SENDA-ACT-AGO2026, SENDA/SOLE DG, Diagonal 38 y
  las hojas de ventas; mapea a Unidades vía maestro-unidades.json
- Board Unidades: +12 columnas (% avance obra, meses atraso, contratista,
  clave catastral, no. oficial, vendedor, broker, venc. autorización,
  fecha entrega obra, 4 checkboxes de hitos documentales) — 147 items
  actualizados (DIAGONAL 38/38, SENDA 109/142)
- Board nuevo 'Flujo Escrituras' (18429252054): 32 items mayo–ago/2026
  por proyecto/semana con escrituras e ingreso
- Board nuevo 'Saldos a Favor' (18429252069): 7 pendientes (Estanza/Sole)
- Receta nativa documentada: recordatorio 7 días antes de Venc.
  autorización al coordinador
- SOLE y MUTUO no existen como inventario en Monday: sus datos de obra
  se reportan como saltados (no se crean unidades)
This commit is contained in:
2026-09-01 21:59:45 +00:00
parent ccd4ca78ca
commit 11213b92c7
4 changed files with 609 additions and 1 deletions

View File

@@ -0,0 +1,572 @@
#!/usr/bin/env python3
"""Sync de obra/estatus del Reporte_EstatusVenta.xlsx -> Monday (workspace Frasa-CAS).
Fuentes (hojas del Excel) y destino:
- ESTATUS OBRA -> Unidades: % Avance obra, Contratista, Fecha entrega obra (SENDA; SOLE se salta)
- SENDA-ACT-AGO2026. -> Unidades SENDA: Fecha entrega obra, hitos (checkbox por fecha <= hoy),
No. oficial / Clave catastral (texto; el Excel trae fechas programadas)
- SENDA DG -> Unidades SENDA: Meses atraso, % Avance obra (fallback si ESTATUS OBRA no cubrio)
- Diagonal 38 -> Unidades DIAGONAL: hitos, No. oficial, Clave catastral, Fecha entrega obra
- Mutuo -> solo reporte (MUTUO no esta en el board Unidades)
- Solana/Atelier/Estanza/Senda/Sole -> board "Saldos a Favor" (saldo > 0)
- Flujo Calendario -> board "Flujo Escrituras" (item por mes/semana/proyecto)
Idempotente: lee valores actuales y solo muta lo que cambio; en boards nuevos la
llave es Unidad+Desarrollo (saldos) o el name unico (flujo). Checkboxes solo se
encienden (nunca se apagan). Nada de PII se versiona: los datos viven solo en Monday.
Uso:
python3 sync-obra-estatus.py # aplica cambios
python3 sync-obra-estatus.py --dry-run # solo reporta
La API key se toma de MONDAY_API_KEY o de frasa-ecosystem/.env.
Rate limit: ~0.4s entre mutaciones, retry con backoff ante 429/5xx.
"""
import json, os, re, sys, time, urllib.request, urllib.error
from datetime import date, datetime
# ---------------- IDs de Monday (creados 2026-09-01 via API) ----------------
BOARD_UNIDADES = 18427678487
COL = {
'desarrollo': 'text_mm6ehqqb',
'avance': 'numeric_mm6sdh3c', # % Avance obra
'atraso': 'numeric_mm6s6bkz', # Meses atraso
'contratista': 'text_mm6s3992',
'catastral': 'text_mm6s5tb6', # Clave catastral
'noficial': 'text_mm6s7zan', # No. oficial
'vendedor': 'text_mm6s4m51',
'broker': 'text_mm6sxzn8',
'venc_aut': 'date_mm6s3pyx', # Venc. autorización
'fentrega_obra': 'date_mm6sw443', # Fecha entrega obra (date_mm6empjv ya tenia datos)
'gas': 'boolean_mm6s2fze', # Cert. Gas
'toc4': 'boolean_mm6s8crm', # TO / C4
'regimen': 'boolean_mm6sktee', # Régimen inscrito
'avaluo': 'boolean_mm6spj1y', # Lista avalúo
}
BOARD_FLUJO = 18429252054
COL_FLUJO = {
'proyecto': 'color_mm6scbgh', 'mes': 'text_mm6s19pw', 'semana': 'text_mm6sa5t7',
'fecha': 'date_mm6ssf9s', 'escrituras': 'numeric_mm6sqraa', 'ingreso': 'numeric_mm6swy2f',
}
BOARD_SALDOS = 18429252069
COL_SALDOS = {
'desarrollo': 'color_mm6smmyz', 'unidad': 'text_mm6sr7d2', 'monto': 'numeric_mm6spv2d',
'estado': 'color_mm6sbebg', 'comentario': 'text_mm6sa9hz',
}
API = 'https://api.monday.com/v2'
DRY = '--dry-run' in sys.argv
HOY = date.today()
ROOT = os.path.dirname(os.path.abspath(__file__))
EXCEL = '/root/Reporte_EstatusVenta.xlsx'
MAESTRO_JSON = os.path.join(ROOT, '../../reference/maestro/maestro-unidades.json')
PROYECTOS = ['ATELIER', 'ESTANZA', 'SOLE', 'SOLANA', 'SENDA', 'MUTUO']
MESES = {'ene': 1, 'feb': 2, 'mar': 3, 'abr': 4, 'may': 5, 'jun': 6,
'jul': 7, 'ago': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dic': 12}
MESES_FLOJO = {'MAYO': 5, 'JUNIO': 6, 'JULIO': 7, 'AGOSTO': 8}
saltadas = [] # (unidad, fuente, motivo)
def load_key():
key = os.environ.get('MONDAY_API_KEY')
if key:
return key
env = os.path.join(ROOT, '../../.env')
for line in open(env):
if line.startswith('MONDAY_API_KEY='):
return line.strip().split('=', 1)[1].strip().strip('"').strip("'")
sys.exit('MONDAY_API_KEY no encontrada')
KEY = load_key()
def gql(query, variables=None, reintentos=3):
body = json.dumps({'query': query, 'variables': variables or {}}).encode()
for intento in range(reintentos + 1):
if intento:
time.sleep(0.5 * 2 ** (intento - 1))
req = urllib.request.Request(API, data=body, headers={
'Content-Type': 'application/json', 'Authorization': KEY, 'API-Version': '2024-10'})
try:
res = json.load(urllib.request.urlopen(req))
except urllib.error.HTTPError as e:
if e.code in (429, 500, 502, 503) and intento < reintentos:
continue
raise
if res.get('errors'):
if intento < reintentos:
continue
raise RuntimeError(res['errors'])
return res['data']
raise RuntimeError('Monday no respondió')
def pausa():
if not DRY:
time.sleep(0.4)
# ---------------- parsers tolerantes ----------------
def parse_fecha(v):
"""datetime/str ('22-jun.-26', 'vie 09/04/27', '17/07/206') -> date o None."""
if isinstance(v, datetime):
return v.date() if v.date().year > 2000 else None
if isinstance(v, date):
return v
if not isinstance(v, str):
return None
s = v.strip().lower().replace('\xa0', ' ')
m = re.search(r'(\d{1,2})\s*[/-]\s*([a-záé.]+|\d{1,2})\s*[/-]\s*(\d{2,4})', s)
if not m:
return None
d, mes, y = m.group(1), m.group(2).rstrip('.'), m.group(3)
try:
mes = MESES[mes[:3]] if not mes.isdigit() else int(mes)
y = int(y)
y = 2000 + (y % 100) if y < 1000 else y
return date(y, mes, int(d))
except (KeyError, ValueError):
return None
def parse_numero(v):
"""int/float o texto '$4,306,500.00' -> float o None (#REF!, #VALUE!, vacio)."""
if isinstance(v, (int, float)):
return float(v)
if not isinstance(v, str):
return None
s = v.replace('\xa0', '').strip()
if not s or s.startswith('#'):
return None
s = s.replace('$', '').replace(',', '').strip()
try:
return float(s)
except ValueError:
return None
def parse_pct(v):
"""'48.6%' o 0.486 o 48.6 -> 48.6."""
if isinstance(v, str):
v = parse_numero(v.replace('%', ''))
if v is None:
return None
elif not isinstance(v, (int, float)):
return None
v = float(v)
return v * 100 if 0 < v <= 1 else v
def norm_clave(v):
"""Normaliza mza/lote/interior: 26.0 -> '26', '01' -> '1', ' 7A ' -> '7A'."""
if v is None:
return None
if isinstance(v, float) and v.is_integer():
v = int(v)
s = str(v).strip().replace('\xa0', '')
return s.lstrip('0') or '0' if s.isdigit() else s
def hito(v):
"""Checkbox por fecha del hito: True si la fecha ya llego; None si no aplica."""
f = parse_fecha(v)
return True if f and f <= HOY else None
# ---------------- Monday: lectura ----------------
def fetch_items(board_id, col_ids):
items, cursor = [], None
sel = ' column_values(ids: [%s]) { id text }' % ','.join('"%s"' % c for c in col_ids) if col_ids else ''
while True:
q = ('query($cursor: String) { boards(ids: %d) { items_page(limit: 200, cursor: $cursor) '
'{ cursor items { id name%s } } } }' % (board_id, sel))
page = gql(q, {'cursor': cursor})['boards'][0]['items_page']
items += page['items']
cursor = page['cursor']
if not cursor or not page['items']:
break
time.sleep(0.4)
return [{'id': i['id'], 'name': i['name'],
'cols': {c['id']: c['text'] for c in i.get('column_values', [])}} for i in items]
def cambiar_valores(item_id, board_id, valores):
q = ('mutation($b: ID!, $i: ID!, $v: JSON!) { change_multiple_column_values('
'board_id: $b, item_id: $i, column_values: $v, create_labels_if_missing: true) { id } }')
gql(q, {'b': str(board_id), 'i': str(item_id), 'v': json.dumps(valores)})
def crear_item(board_id, nombre, valores):
q = ('mutation($b: ID!, $n: String!, $v: JSON!) { create_item('
'board_id: $b, item_name: $n, column_values: $v, create_labels_if_missing: true) { id } }')
return gql(q, {'b': str(board_id), 'n': nombre, 'v': json.dumps(valores)})['create_item']['id']
# ---------------- staging de Unidades ----------------
def valor_monday(col_key, v):
if col_key in ('fentrega_obra', 'venc_aut'):
return {'date': v.isoformat()}
if col_key in ('gas', 'toc4', 'regimen', 'avaluo'):
return {'checked': True}
if col_key in ('avance', 'atraso'):
return v
return str(v)
def igual(col_key, actual, deseado):
if col_key in ('avance', 'atraso'):
try:
return abs(float(actual) - float(deseado)) < 1e-6
except (TypeError, ValueError):
return False
if col_key in ('fentrega_obra', 'venc_aut'):
return actual == deseado.isoformat()
if col_key in ('gas', 'toc4', 'regimen', 'avaluo'):
return (actual or '').lower() in ('true', '✓', 'v', '1')
return (actual or '') == str(deseado)
class SyncUnidades:
"""Acumula cambios por item y aplica solo lo que difiere del board."""
def __init__(self):
self.items = {} # name -> {'id', 'cols'}
self.cambios = {} # item_id -> {col_key: valor}
self.avance_cubierto = set() # items con avance desde ESTATUS OBRA
def cargar(self, items):
self.items = {i['name']: i for i in items}
def set(self, codigo, col_key, valor, fuente, sobrescribir=True):
if isinstance(valor, str):
valor = valor.replace('\xa0', ' ').strip()
if valor is None or valor == '':
return
item = self.items.get(codigo)
if not item:
saltadas.append((codigo, fuente, 'no existe en board Unidades'))
return
if col_key == 'avance':
if not sobrescribir and codigo in self.avance_cubierto:
return
if sobrescribir:
self.avance_cubierto.add(codigo)
staged = self.cambios.setdefault(item['id'], {})
if igual(col_key, item['cols'].get(COL[col_key]), valor):
# la fuente mas reciente coincide con el board: descarta lo ya acumulado
staged.pop(col_key, None)
if not staged:
del self.cambios[item['id']]
return
staged[col_key] = valor
def aplicar(self):
for item_id, cols in sorted(self.cambios.items()):
valores = {COL[k]: valor_monday(k, v) for k, v in cols.items()}
nombre = next(i['name'] for i in self.items.values() if i['id'] == item_id)
print(' %s: %s%s' % (nombre, ', '.join(sorted(cols)), ' [dry-run]' if DRY else ''))
if not DRY:
cambiar_valores(item_id, BOARD_UNIDADES, valores)
pausa()
# ---------------- fuentes: hojas del Excel ----------------
def leer_estatus_obra(wb, sync):
ws = wb['ESTATUS OBRA']
proyecto = None
for i, r in enumerate(ws.iter_rows(min_row=3, max_row=54, max_col=19, values_only=True), 3):
marca = next((str(c).upper() for c in r[:3] if isinstance(c, str) and 'PROYECTO' in c.upper()), None)
if marca:
proyecto = 'SOLE' if 'SOLE' in marca else 'SENDA' if 'SENDA' in marca else proyecto
continue
viv = r[3]
if viv is None or not str(viv).strip():
continue
if proyecto == 'SOLE':
saltadas.append(('SOLE-%s' % norm_clave(viv), 'ESTATUS OBRA', 'SOLE no esta en board Unidades'))
continue
m = re.match(r'M-(\d+)-L(\d+)-(\w+)', str(viv).strip())
if not m:
saltadas.append((str(viv), 'ESTATUS OBRA fila %d' % i, 'clave no reconocida'))
continue
codigo = 'SENDA-%d-%d-%s' % (int(m.group(1)), int(m.group(2)), norm_clave(m.group(3)))
sync.set(codigo, 'avance', parse_pct(r[8]), 'ESTATUS OBRA')
sync.set(codigo, 'contratista', str(r[6]).strip() if r[6] else None, 'ESTATUS OBRA')
sync.set(codigo, 'fentrega_obra', parse_fecha(r[12]), 'ESTATUS OBRA')
def leer_senda_act(wb, sync):
ws = wb['SENDA-ACT-AGO2026.']
for i, r in enumerate(ws.iter_rows(min_row=4, max_row=126, max_col=12, values_only=True), 4):
unidad = r[3]
if unidad is None or not str(unidad).strip():
continue
m = re.match(r'\s*Mza\s*(\w+)\s*L(?:ote)?\s*-?\s*(\w+)(?:\s*int\s*(\w+))?', str(unidad), re.I)
if not m:
saltadas.append((str(unidad), 'SENDA-ACT fila %d' % i, 'clave no reconocida'))
continue
mza, lote, interior = norm_clave(m.group(1)), norm_clave(m.group(2)), norm_clave(m.group(3)) or '1'
codigo = resolver_senda(mza, lote, interior, preferir_directo=True)
if not codigo:
saltadas.append((str(unidad), 'SENDA-ACT fila %d' % i, 'sin mapeo a codigo_nuestro'))
continue
sync.set(codigo, 'fentrega_obra', parse_fecha(r[5]), 'SENDA-ACT')
sync.set(codigo, 'gas', hito(r[6]), 'SENDA-ACT')
sync.set(codigo, 'toc4', hito(r[7]), 'SENDA-ACT')
sync.set(codigo, 'regimen', hito(r[8]), 'SENDA-ACT')
f_no, f_cc = parse_fecha(r[9]), parse_fecha(r[10])
sync.set(codigo, 'noficial', f_no.isoformat() if f_no else r[9], 'SENDA-ACT')
sync.set(codigo, 'catastral', f_cc.isoformat() if f_cc else r[10], 'SENDA-ACT')
sync.set(codigo, 'avaluo', hito(r[11]), 'SENDA-ACT')
def leer_senda_dg(wb, sync):
ws = wb['SENDA DG']
for i, r in enumerate(ws.iter_rows(min_row=3, max_row=143, max_col=14, values_only=True), 3):
ubic, mz, lt = norm_clave(r[3]), norm_clave(r[4]), norm_clave(r[5])
if not mz or not lt or not ubic:
continue
if '#' in mz + lt + ubic:
saltadas.append(('%s-%s-%s' % (mz, lt, ubic), 'SENDA DG fila %d' % i, '#REF! en clave'))
continue
codigo = resolver_senda(mz, lt, ubic)
if not codigo:
saltadas.append(('SENDA-%s-%s-%s' % (mz, lt, ubic), 'SENDA DG fila %d' % i, 'sin mapeo'))
continue
atraso = parse_numero(r[9])
sync.set(codigo, 'atraso', atraso, 'SENDA DG')
sync.set(codigo, 'avance', parse_pct(r[10]), 'SENDA DG', sobrescribir=False)
def leer_diagonal(wb, sync):
ws = wb['Diagonal 38']
for i, r in enumerate(ws.iter_rows(min_row=3, max_row=40, max_col=12, values_only=True), 3):
if r[2] is None:
continue
codigo = 'U-%s' % norm_clave(r[2])
sync.set(codigo, 'gas', hito(r[5]), 'Diagonal 38')
sync.set(codigo, 'toc4', hito(r[6]), 'Diagonal 38')
sync.set(codigo, 'regimen', hito(r[7]), 'Diagonal 38')
f_no, f_cc = parse_fecha(r[8]), parse_fecha(r[9])
sync.set(codigo, 'noficial', f_no.isoformat() if f_no else r[8], 'Diagonal 38')
sync.set(codigo, 'catastral', f_cc.isoformat() if f_cc else r[9], 'Diagonal 38')
sync.set(codigo, 'avaluo', hito(r[10]), 'Diagonal 38')
sync.set(codigo, 'fentrega_obra', parse_fecha(r[11]), 'Diagonal 38')
def leer_mutuo(wb):
ws = wb['Mutuo']
n = 0
for i, r in enumerate(ws.iter_rows(min_row=9, max_row=32, max_col=29, values_only=True), 9):
if r[25] is not None or r[27] is not None:
n += 1
print('Mutuo: %d filas con venc. autorizacion/vendedor; MUTUO no esta en board Unidades '
'-> vendedor/broker/venc. no se cargan esta corrida' % n)
# ---------------- maestro ----------------
def resolver_senda(mza, lote, interior, preferir_directo=False):
directo = 'SENDA-%s-%s-%s' % (mza, lote, interior)
if preferir_directo and directo in BOARD_NAMES:
return directo
codigo = MAESTRO.get((mza, lote, interior))
if codigo and codigo in BOARD_NAMES:
return codigo
if directo in BOARD_NAMES:
return directo
# caso invertido del maestro (ej. Excel mza/lote/int=410/1/B201 -> maestro 410/B201/1)
codigo = MAESTRO.get((mza, interior, lote))
if codigo and codigo in BOARD_NAMES:
return codigo
return None
# ---------------- Saldos a Favor ----------------
def sync_saldos(wb):
hojas = {'Solana I y II': 'SOLANA', 'Atelier': 'ATELIER', 'Estanza': 'ESTANZA',
'Senda': 'SENDA', 'Sole': 'SOLE'}
existentes = fetch_items(BOARD_SALDOS, list(COL_SALDOS.values()))
por_llave = {(e['cols'].get(COL_SALDOS['desarrollo']), e['cols'].get(COL_SALDOS['unidad'])): e
for e in existentes}
creados = actualizados = sin_cambio = 0
for hoja, desarrollo in hojas.items():
ws = wb[hoja]
rows = list(ws.iter_rows(min_row=4, max_row=min(ws.max_row, 500),
max_col=min(ws.max_column, 40), values_only=True))
if not rows:
continue
hdr = {str(v).strip().upper(): j for j, v in enumerate(rows[0]) if v is not None}
c_saldo = next((j for k, j in hdr.items() if k.startswith('SALDO A FAVOR')), None)
c_nombre = hdr.get('NOMBRE')
c_unidad = hdr.get('UNIDADES', hdr.get('UNIDAD'))
c_com = next((j for k, j in hdr.items() if k.startswith('COMENTARIO')), None)
if c_saldo is None or c_nombre is None:
continue
for i, r in enumerate(rows[1:], 5):
monto = parse_numero(r[c_saldo])
if not monto or monto <= 0:
continue
nombre = str(r[c_nombre]).strip() if r[c_nombre] else '?'
if c_unidad is not None and r[c_unidad] is not None:
unidad = str(r[c_unidad]).strip()
elif desarrollo == 'SENDA':
unidad = '%s-%s-%s' % (norm_clave(r[hdr.get('MZA', -1)]), norm_clave(r[hdr.get('LOTE', -1)]),
norm_clave(r[hdr.get('UBICACIÓN', -1)]))
else:
unidad = '?'
comentario = str(r[c_com]).strip() if c_com is not None and r[c_com] else None
llave = (desarrollo, unidad)
if llave in por_llave:
e = por_llave[llave]
dif = {}
if not igual('monto', e['cols'].get(COL_SALDOS['monto']), monto):
dif[COL_SALDOS['monto']] = monto
if comentario and (e['cols'].get(COL_SALDOS['comentario']) or '') != comentario:
dif[COL_SALDOS['comentario']] = comentario
if dif:
print(' saldo %s/%s: actualiza %s%s' % (desarrollo, unidad, sorted(dif),
' [dry-run]' if DRY else ''))
if not DRY:
cambiar_valores(e['id'], BOARD_SALDOS, dif)
pausa()
actualizados += 1
else:
sin_cambio += 1
continue
valores = {COL_SALDOS['desarrollo']: {'label': desarrollo},
COL_SALDOS['unidad']: unidad, COL_SALDOS['monto']: monto,
COL_SALDOS['estado']: {'label': 'Pendiente'}}
if comentario:
valores[COL_SALDOS['comentario']] = comentario
print(' saldo %s / %s: %s -> crear%s' % (nombre, unidad, monto, ' [dry-run]' if DRY else ''))
if not DRY:
crear_item(BOARD_SALDOS, '%s / %s' % (nombre, unidad), valores)
pausa()
creados += 1
print('Saldos a Favor: %d creados, %d actualizados, %d sin cambio' % (creados, actualizados, sin_cambio))
# ---------------- Flujo Escrituras ----------------
def sync_flujo(wb):
ws = wb['Flujo Calendario']
rows = list(ws.iter_rows(min_row=1, max_row=160, max_col=15, values_only=True))
mes = None
semana = None
resumen = {} # mes -> {proyecto: (escrituras, ingreso)}
detalle = [] # (mes, semana, fecha, proyecto, escrituras, ingreso)
for r in rows:
c = list(r) + [None] * (15 - len(r))
v5 = str(c[4]).strip().upper() if isinstance(c[4], str) else None
if v5 in MESES_FLOJO:
mes, semana = v5, None
continue
if mes is None:
continue
v1 = str(c[0]).strip().upper() if isinstance(c[0], str) else None
if v1 in PROYECTOS: # resumen mensual A:C
resumen.setdefault(mes, {})[v1] = (parse_numero(c[1]), parse_numero(c[2]))
dias = [int(v) for v in c[5:12] if isinstance(v, (int, float)) and float(v).is_integer()
and 1 <= int(v) <= 31]
v13 = c[12]
es_encabezado = (isinstance(v13, str) and v13.strip().upper().startswith('SEM')) or \
isinstance(v13, datetime) or (dias and c[1] is None and c[4] is None)
if es_encabezado and dias:
if isinstance(v13, str) and v13.strip().upper().startswith('SEM'):
semana = re.sub(r'\s+%s\s*$' % mes, '', v13.strip().upper())
else:
semana = '%d AL %d' % (min(dias), max(dias)) if len(dias) > 1 else 'DIA %d' % dias[0]
semana = (semana, date(2026, MESES_FLOJO[mes], min(dias)))
continue
if semana and v5 in PROYECTOS:
esc = parse_numero(c[2]) if isinstance(c[1], str) and c[1].strip().upper() == v5 else None
detalle.append((mes, semana[0], semana[1], v5, esc, parse_numero(v13)))
items = {}
for mes_n, sem, fecha, proy, esc, ing in detalle:
if not esc and not ing:
continue
items[(mes_n, sem, proy)] = {'mes': mes_n, 'semana': sem, 'fecha': fecha,
'proyecto': proy, 'escrituras': esc, 'ingreso': ing}
for mes_n, proys in resumen.items(): # meses sin desglose semanal -> resumen
if any(k[0] == mes_n for k in items):
continue
for proy, (esc, ing) in proys.items():
if not esc and not ing:
continue
items[(mes_n, 'Mes completo', proy)] = {'mes': mes_n, 'semana': 'Mes completo',
'fecha': date(2026, MESES_FLOJO[mes_n], 1), 'proyecto': proy,
'escrituras': esc, 'ingreso': ing}
existentes = {i['name'] for i in fetch_items(BOARD_FLUJO, [])}
creados = ya = 0
for d in sorted(items.values(), key=lambda x: (x['fecha'], x['proyecto'])):
nombre = '%s %s - %s' % (d['mes'], d['semana'], d['proyecto'])
if nombre in existentes:
ya += 1
continue
valores = {COL_FLUJO['proyecto']: {'label': d['proyecto']}, COL_FLUJO['mes']: d['mes'],
COL_FLUJO['semana']: d['semana'], COL_FLUJO['fecha']: {'date': d['fecha'].isoformat()}}
if d['escrituras'] is not None:
valores[COL_FLUJO['escrituras']] = d['escrituras']
if d['ingreso'] is not None:
valores[COL_FLUJO['ingreso']] = d['ingreso']
print(' flujo %s: esc=%s ing=%s%s' % (nombre, d['escrituras'], d['ingreso'],
' [dry-run]' if DRY else ''))
if not DRY:
crear_item(BOARD_FLUJO, nombre, valores)
pausa()
creados += 1
print('Flujo Escrituras: %d creados, %d ya existian' % (creados, ya))
# ---------------- main ----------------
def main():
import openpyxl
global BOARD_NAMES, MAESTRO
wb = openpyxl.load_workbook(EXCEL, data_only=True, read_only=True)
maestro = json.load(open(MAESTRO_JSON))
MAESTRO = {(norm_clave(r['clave_maestro']['mza']), norm_clave(r['clave_maestro']['lote']),
norm_clave(r['clave_maestro']['interior'])): r['codigo_nuestro']
for r in maestro if r['desarrollo'] == 'SENDA'}
sync = SyncUnidades()
sync.cargar(fetch_items(BOARD_UNIDADES, list(COL.values())))
BOARD_NAMES = set(sync.items)
print('Board Unidades: %d items' % len(BOARD_NAMES))
leer_estatus_obra(wb, sync)
leer_senda_act(wb, sync)
leer_senda_dg(wb, sync)
leer_diagonal(wb, sync)
leer_mutuo(wb)
from collections import Counter
print('\n== Unidades: %d items con cambios ==' % len(sync.cambios))
stats = Counter()
for item_id, cols in sync.cambios.items():
for k in cols:
stats[k] += 1
for k, n in stats.most_common():
print(' %s: %d' % (k, n))
sync.aplicar()
print('\n== Saldos a Favor ==')
sync_saldos(wb)
print('\n== Flujo Escrituras ==')
sync_flujo(wb)
print('\n== Unidades saltadas (%d) ==' % len(saltadas))
for codigo, fuente, motivo in saltadas:
print(' %s [%s]: %s' % (codigo, fuente, motivo))
if __name__ == '__main__':
main()