- Add "Mis cortes de caja" report for cashiers/counters with sales detail. - Cash register history scoped to own cuts for non-admin roles; new /register/<id>/sales endpoint. - Remove dashboard from cashier menu; add Reports to cashier/counter. - Service orders: assign mechanic, budget field, invoice flag, counter/cashier can add items/remissions, convert to remission. - Remission notes module (UI, CSS, courier, counter remissions). - Customer hard-delete and vehicle/customer linkage in workshop. - POS: always show search results, compact payment grid, credit validation, tier pricing (5%/10%), ticket with customer/folio. - Inventory: CSV template with sku_secondary, alias import. - Rached migration scripts and DB migrations. - Version-bump cached JS/CSS query strings. Excludes local Rached session tokens/captures (rached_*.json / rached_*.txt).
92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Retry fetching missing Rached client details with slower rate."""
|
|
import json
|
|
import os
|
|
import time
|
|
import requests
|
|
from pathlib import Path
|
|
|
|
BASE_API = 'http://appapi.flechasyventiladores-rached.com'
|
|
OUT_DIR = Path(os.environ.get('OUT_DIR', Path(__file__).resolve().parent.parent / 'data' / 'rached_import'))
|
|
PASS = 'Nexus01'
|
|
|
|
HEADERS = {
|
|
'Accept': 'application/json, text/plain, */*',
|
|
'Content-Type': 'application/json',
|
|
'Origin': 'http://app.flechasyventiladores-rached.com',
|
|
'Referer': 'http://app.flechasyventiladores-rached.com/',
|
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
|
|
}
|
|
|
|
|
|
def login():
|
|
url = f'{BASE_API}/api/seguridad/login'
|
|
payload = {
|
|
'grant_type': 'password',
|
|
'client_secret': 'JiRE9iL3pqRnqcFp6wDeYH0tYu97QSpkrwVKAvEP',
|
|
'client_id': 2,
|
|
'username': 'IVAN@flechasyventiladores-rached.com',
|
|
'password': PASS,
|
|
}
|
|
resp = requests.post(url, headers=HEADERS, json=payload)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
if data.get('mensaje'):
|
|
raise RuntimeError(f'Login failed: {data["mensaje"]}')
|
|
return data['datos']['token']
|
|
|
|
|
|
def fetch_client(token, cid, attempts=5):
|
|
url = f'{BASE_API}/api/catalogos/clientes/{cid}'
|
|
for attempt in range(attempts):
|
|
resp = requests.get(url, headers={**HEADERS, 'Authorization': f'Bearer {token}'}, timeout=30)
|
|
if resp.status_code == 429:
|
|
wait = 5 + attempt * 3
|
|
print(f' client {cid} 429, waiting {wait}s')
|
|
time.sleep(wait)
|
|
continue
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
raise RuntimeError(f'client {cid} rate limited')
|
|
|
|
|
|
def main():
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
detail_dir = OUT_DIR / 'client_details'
|
|
detail_dir.mkdir(exist_ok=True)
|
|
|
|
needed = set()
|
|
for f in OUT_DIR.glob('details/order_*.json'):
|
|
d = json.loads(f.read_text(encoding='utf-8')).get('datos', {})
|
|
cid = d.get('idCliente')
|
|
if cid:
|
|
needed.add(cid)
|
|
|
|
saved = set(int(p.name.split('_')[1].split('.')[0]) for p in detail_dir.glob('client_*.json'))
|
|
missing = sorted(needed - saved)
|
|
print(f'Need {len(needed)}, have {len(saved)}, missing {len(missing)}')
|
|
if not missing:
|
|
return
|
|
|
|
token = login()
|
|
results = json.loads((OUT_DIR / 'catalog_client_details.json').read_text(encoding='utf-8')) if (OUT_DIR / 'catalog_client_details.json').exists() else {}
|
|
|
|
for idx, cid in enumerate(missing, 1):
|
|
try:
|
|
data = fetch_client(token, cid)
|
|
except Exception as e:
|
|
print(f'[{idx}/{len(missing)}] client {cid} final error: {e}')
|
|
continue
|
|
detail_path = detail_dir / f'client_{cid}.json'
|
|
detail_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding='utf-8')
|
|
results[str(cid)] = data.get('datos')
|
|
print(f'[{idx}/{len(missing)}] client {cid} saved')
|
|
time.sleep(1.2)
|
|
|
|
(OUT_DIR / 'catalog_client_details.json').write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding='utf-8')
|
|
print(f'Saved {len(results)} client details')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|