feat: cashier/counter reports, service-order & remission flows, Rached migration utils
- 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).
This commit is contained in:
144
scripts/scrape_rached_workshop.py
Normal file
144
scripts/scrape_rached_workshop.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scrape service orders from Rached legacy web app using their internal API."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
BASE_APP = 'http://app.flechasyventiladores-rached.com'
|
||||
BASE_API = 'http://appapi.flechasyventiladores-rached.com'
|
||||
OUT_DIR = Path(os.environ.get('OUT_DIR', Path(__file__).resolve().parent.parent / 'data' / 'rached_import'))
|
||||
USER = os.environ.get('RACHED_USER', 'IVAN')
|
||||
PASS = os.environ.get('RACHED_PASS', 'Nexus01')
|
||||
LIMIT = int(os.environ.get('LIMIT', '0'))
|
||||
|
||||
HEADERS = {
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Origin': BASE_APP,
|
||||
'Referer': f'{BASE_APP}/',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/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 api_headers(token):
|
||||
return {**HEADERS, 'Authorization': f'Bearer {token}'}
|
||||
|
||||
|
||||
def fetch_list(token, page):
|
||||
url = f'{BASE_API}/api/servicios/ordenes/listado?page={page}'
|
||||
resp = requests.post(url, headers=api_headers(token))
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def fetch_detail(token, order_id):
|
||||
url = f'{BASE_API}/api/servicios/ordenes/{order_id}'
|
||||
resp = requests.get(url, headers=api_headers(token))
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def main():
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
log_path = OUT_DIR / 'scrape_py.log'
|
||||
|
||||
def log(msg):
|
||||
print(msg)
|
||||
with open(log_path, 'a', encoding='utf-8') as f:
|
||||
f.write(msg + '\n')
|
||||
|
||||
log('Logging in...')
|
||||
token = login()
|
||||
log('Logged in.')
|
||||
|
||||
# ── List ──
|
||||
list_path = OUT_DIR / 'orders_list.json'
|
||||
if list_path.exists():
|
||||
all_orders = json.loads(list_path.read_text(encoding='utf-8'))
|
||||
log(f'Loaded {len(all_orders)} orders from existing list.')
|
||||
else:
|
||||
all_orders = []
|
||||
page = 1
|
||||
while True:
|
||||
for attempt in range(3):
|
||||
try:
|
||||
data = fetch_list(token, page)
|
||||
break
|
||||
except Exception as e:
|
||||
log(f'List page {page} attempt {attempt+1} error: {e}')
|
||||
time.sleep(2)
|
||||
else:
|
||||
log(f'Failed to fetch list page {page} after 3 attempts.')
|
||||
break
|
||||
|
||||
datos = data.get('datos', {})
|
||||
orders = datos.get('data', [])
|
||||
all_orders.extend(orders)
|
||||
(OUT_DIR / f'orders_page_{page}.json').write_text(
|
||||
json.dumps(data, indent=2, ensure_ascii=False),
|
||||
encoding='utf-8'
|
||||
)
|
||||
log(f'Page {page}: {len(orders)} orders (total {len(all_orders)}/{datos.get("total", "?")})')
|
||||
if datos.get('current_page', 1) >= datos.get('last_page', 1):
|
||||
break
|
||||
page += 1
|
||||
time.sleep(1)
|
||||
|
||||
list_path.write_text(json.dumps(all_orders, indent=2, ensure_ascii=False), encoding='utf-8')
|
||||
log(f'Collected {len(all_orders)} orders summary.')
|
||||
|
||||
if not all_orders:
|
||||
log('No orders to fetch details for.')
|
||||
return
|
||||
|
||||
# ── Details ──
|
||||
detail_dir = OUT_DIR / 'details'
|
||||
detail_dir.mkdir(exist_ok=True)
|
||||
|
||||
target_orders = all_orders[:LIMIT] if LIMIT > 0 else all_orders
|
||||
total = len(target_orders)
|
||||
for i, o in enumerate(target_orders, start=1):
|
||||
oid = o['id']
|
||||
detail_path = detail_dir / f'order_{oid}.json'
|
||||
if detail_path.exists():
|
||||
log(f'[{i}/{total}] {oid} already saved')
|
||||
continue
|
||||
for attempt in range(3):
|
||||
try:
|
||||
data = fetch_detail(token, oid)
|
||||
detail_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding='utf-8')
|
||||
log(f'[{i}/{total}] {oid} saved')
|
||||
break
|
||||
except Exception as e:
|
||||
log(f'[{i}/{total}] {oid} attempt {attempt+1} error: {e}')
|
||||
time.sleep(2)
|
||||
else:
|
||||
log(f'[{i}/{total}] {oid} FAILED after 3 attempts')
|
||||
time.sleep(1)
|
||||
|
||||
log('Done.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user