feat(inventario): sincronización de estatus con reporte maestro Enkontrol (AGO26)
- 16 unidades actualizadas en Monday y en seeds del cotizador: apartados recientes (DIAGONAL U-102/U-107, SENDA, ESTANZA-61, SOLANA-2A), escrituradas (ATELIER-9), ventas confirmadas (ESTANZA-33/42/46, SENDA 409-1-10) - Script idempotente sync-estatus-maestro.py + análisis reproducible (reference/, sin PII) - Tests cotizador 61/61 verdes
This commit is contained in:
121
api-gateway/scripts/sync-estatus-maestro.py
Normal file
121
api-gateway/scripts/sync-estatus-maestro.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sync de estatus del Reporte Maestro (Enkontrol) -> board Unidades de Monday (18427678487).
|
||||
|
||||
Idempotente: lee reference/maestro/maestro-unidades.json, consulta el board y solo
|
||||
muta items cuyo label de 'Disponibilidad' difiera del status_maestro. Excluye:
|
||||
- unidades sin codigo_nuestro (sin match en nuestro inventario)
|
||||
- unidades con baja_confianza_status (apartado sin fec_liberacion)
|
||||
- items ya en el status correcto
|
||||
|
||||
Uso:
|
||||
MONDAY_API_KEY=... python3 sync-estatus-maestro.py # aplica cambios
|
||||
MONDAY_API_KEY=... python3 sync-estatus-maestro.py --dry-run # solo reporta
|
||||
|
||||
La API key también se toma de frasa-ecosystem/.env si no está en el entorno.
|
||||
Rate limit: ~0.4s entre mutaciones, retry con backoff ante 429/5xx.
|
||||
SIN PII: solo códigos de unidad y status.
|
||||
"""
|
||||
import json, os, sys, time, urllib.request, urllib.error
|
||||
|
||||
BOARD_ID = 18427678487
|
||||
COL_STATUS = 'color_mm6edzbb' # 'Disponibilidad'
|
||||
COL_DESARROLLO = 'text_mm6ehqqb'
|
||||
API = 'https://api.monday.com/v2'
|
||||
DRY = '--dry-run' in sys.argv
|
||||
|
||||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
MAESTRO_JSON = os.path.join(ROOT, '../../reference/maestro/maestro-unidades.json')
|
||||
|
||||
|
||||
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 fetch_items():
|
||||
items, cursor = [], None
|
||||
while True:
|
||||
q = ('query($cursor: String) { boards(ids: %d) { items_page(limit: 200, cursor: $cursor) '
|
||||
'{ cursor items { id name column_values(ids: ["%s", "%s"]) { id text } } } } }'
|
||||
% (BOARD_ID, COL_STATUS, COL_DESARROLLO))
|
||||
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 {(next(c['text'] for c in i['column_values'] if c['id'] == COL_DESARROLLO), i['name']): {
|
||||
'id': i['id'],
|
||||
'status': next(c['text'] for c in i['column_values'] if c['id'] == COL_STATUS)}
|
||||
for i in items}
|
||||
|
||||
|
||||
def set_status(item_id, label):
|
||||
q = ('mutation($b: ID!, $i: ID!, $v: JSON!) { '
|
||||
'change_column_value(board_id: $b, item_id: $i, column_id: "%s", value: $v) { id } }' % COL_STATUS)
|
||||
gql(q, {'b': str(BOARD_ID), 'i': str(item_id), 'v': json.dumps({'label': label})})
|
||||
|
||||
|
||||
def main():
|
||||
maestro = json.load(open(MAESTRO_JSON))
|
||||
board = fetch_items()
|
||||
cambios, omitidos = [], []
|
||||
for r in maestro:
|
||||
code = r['codigo_nuestro']
|
||||
if not code or r['baja_confianza_status']:
|
||||
continue
|
||||
item = board.get((r['desarrollo'], code))
|
||||
if not item:
|
||||
omitidos.append((code, 'no existe en el board'))
|
||||
continue
|
||||
if item['status'] != r['status_maestro']:
|
||||
cambios.append({'item_id': item['id'], 'codigo': code,
|
||||
'de': item['status'], 'a': r['status_maestro'],
|
||||
'etapa': r['etapa_maestro'], 'fec': r['fec_ultima_etapa']})
|
||||
print(f'{len(cambios)} cambios pendientes, {len(omitidos)} omitidos')
|
||||
aplicados = 0
|
||||
for c in cambios:
|
||||
print(f" {c['codigo']}: {c['de']} -> {c['a']} ({c['etapa']} {c['fec'] or ''})"
|
||||
+ (' [dry-run]' if DRY else ''))
|
||||
if not DRY:
|
||||
set_status(c['item_id'], c['a'])
|
||||
aplicados += 1
|
||||
time.sleep(0.4)
|
||||
if not DRY:
|
||||
print(f'{aplicados} items actualizados en Monday')
|
||||
for code, motivo in omitidos:
|
||||
print(f' omitido {code}: {motivo}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user