feat: cashier/counter reports, service-order & remission flows, Rached migration utils
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- 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:
2026-07-02 12:51:56 +00:00
parent 483498cfcc
commit f42910f4f6
71 changed files with 5388 additions and 626 deletions

View File

@@ -0,0 +1,79 @@
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const http = require('http');
function apiLogin() {
return new Promise((resolve, reject) => {
const payload = JSON.stringify({
grant_type: 'password',
client_secret: 'JiRE9iL3pqRnqcFp6wDeYH0tYu97QSpkrwVKAvEP',
client_id: 2,
username: 'IVAN@flechasyventiladores-rached.com',
password: 'Nexus01'
});
const req = http.request({
hostname: 'appapi.flechasyventiladores-rached.com',
path: '/api/seguridad/login',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': 'http://app.flechasyventiladores-rached.com',
'Referer': 'http://app.flechasyventiladores-rached.com/',
'Content-Length': Buffer.byteLength(payload)
}
}, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
});
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
(async () => {
const outDir = path.resolve(__dirname, '../data/rached_import/har');
fs.mkdirSync(outDir, { recursive: true });
const loginResp = await apiLogin();
const sesion = JSON.stringify(loginResp.datos);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
const allRequests = [];
await page.route('**/*', route => {
const req = route.request();
allRequests.push({ method: req.method(), url: req.url(), headers: req.headers(), postData: req.postData() });
route.continue();
});
await page.goto('http://app.flechasyventiladores-rached.com/', { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
await page.evaluate(s => { localStorage.setItem('sesion', s); }, sesion);
const routes = [
'/administracion/servicios/ordenesservicio',
'/administracion/catalogos/clientes',
'/administracion/catalogos/articulos',
'/administracion/catalogos/mecanicos',
'/administracion/catalogos/sucursales',
'/administracion/catalogos/motociclistas',
'/administracion/catalogos/viasentrega',
'/administracion/catalogos/telefonostipos',
'/administracion/catalogos/direccionestipos',
];
for (const r of routes) {
await page.goto('http://app.flechasyventiladores-rached.com' + r, { waitUntil: 'networkidle' });
await page.waitForTimeout(5000);
}
fs.writeFileSync(path.join(outDir, 'all_requests.json'), JSON.stringify(allRequests, null, 2));
await page.screenshot({ path: path.join(outDir, 'last_screen.png'), fullPage: true });
await browser.close();
console.log('Captured', allRequests.length, 'total requests');
})();

View File

@@ -0,0 +1,442 @@
#!/usr/bin/env python3
"""Import Rached legacy workshop data into the Nexus tenant_refaccionaria_rached DB."""
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
import psycopg2
BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = BASE_DIR / 'data' / 'rached_import'
TENANT_DB = 'tenant_refaccionaria_rached'
# Load DB URL from environment or use localhost defaults
DB_URL = os.environ.get(
'TENANT_DB_URL',
f'postgresql://postgres@localhost/{TENANT_DB}'
)
def normalize_name(s):
if not s:
return ''
return re.sub(r'[^a-z0-9]', '', s.lower().replace('sucursal', '').strip())
def load_json(name):
with open(DATA_DIR / f'{name}.json', encoding='utf-8') as f:
return json.load(f)
def load_detail_json(cid):
path = DATA_DIR / 'client_details' / f'client_{cid}.json'
if path.exists():
with open(path, encoding='utf-8') as f:
return json.load(f)
return None
def connect():
return psycopg2.connect(DB_URL)
ORDER_STATUS_MAP = {
1: 'por_recolectar',
2: 'por_revisar',
3: 'en_revision',
4: 'revisada',
5: 'cotizada',
6: 'por_autorizar',
7: 'autorizada',
8: 'autorizacion_parcial',
9: 'cancelada',
10: 'en_reparacion',
11: 'reparada',
12: 'por_entregar',
13: 'enviado',
14: 'entregado',
15: 'por_facturar',
16: 'facturada',
17: 'por_enviar',
}
ITEM_STATUS_MAP = {
1: 'por_revisar',
2: 'revisando',
3: 'revisado',
4: 'cotizado',
5: 'por_autorizar',
6: 'autorizado',
7: 'cancelado',
8: 'en_reparacion',
9: 'reparado',
10: 'por_entregar',
11: 'enviado',
12: 'entregado',
13: 'por_enviar',
}
def get_default_branch(cur):
cur.execute("SELECT id FROM branches WHERE is_main = true LIMIT 1")
row = cur.fetchone()
if row:
return row[0]
cur.execute("SELECT id FROM branches ORDER BY id LIMIT 1")
return cur.fetchone()[0]
def ensure_branches(cur):
cur.execute("SELECT id, name FROM branches")
existing = {normalize_name(name): id for id, name in cur.fetchall()}
catalog = load_json('catalog_sucursales')['datos']
mapping = {}
for s in catalog:
name = (s.get('nombre') or '').strip()
# Skip the erroneous Rached branch
if name.upper() == 'CREMALLERAS':
continue
key = normalize_name(name)
if key in existing:
mapping[s['id']] = existing[key]
else:
cur.execute(
"INSERT INTO branches (name, is_active) VALUES (%s, true) RETURNING id",
(s['nombre'],)
)
new_id = cur.fetchone()[0]
existing[key] = new_id
mapping[s['id']] = new_id
print(f"Created branch {s['nombre']} -> {new_id}")
return mapping
def ensure_customers(cur, branch_map):
# collect unique client ids referenced in orders
client_ids = set()
for f in DATA_DIR.glob('details/order_*.json'):
d = json.loads(f.read_text(encoding='utf-8')).get('datos', {})
cid = d.get('idCliente')
if cid:
client_ids.add(cid)
# load existing customers by name
cur.execute("SELECT id, name FROM customers")
existing_by_name = {name.strip().lower(): id for id, name in cur.fetchall()}
client_map = {}
default_branch = get_default_branch(cur)
for cid in sorted(client_ids):
detail = load_detail_json(cid)
datos = detail.get('datos', {}) if detail else {}
# Name: prefer 'taller' if meaningful, else full name
taller = (datos.get('taller') or '').strip()
nombre = (datos.get('nombre') or '').strip()
ap1 = (datos.get('primerApellido') or '').strip()
ap2 = (datos.get('segundoApellido') or '').strip()
if taller:
name = taller
else:
name = ' '.join([nombre, ap1, ap2]).strip()
if not name:
name = f"Cliente Rached {cid}"
if name.lower() in existing_by_name:
client_map[cid] = existing_by_name[name.lower()]
continue
# phone / address
phone = None
for t in datos.get('telefonos', []):
num = t.get('numero')
if num:
phone = str(num)
break
address = None
for drec in datos.get('direcciones', []):
address = drec.get('completa')
if address:
break
cur.execute(
"""
INSERT INTO customers (branch_id, name, phone, address, price_tier, is_active)
VALUES (%s, %s, %s, %s, 2, true) RETURNING id
""",
(default_branch, name, phone, address)
)
new_id = cur.fetchone()[0]
existing_by_name[name.lower()] = new_id
client_map[cid] = new_id
print(f"Created customer {cid}: {name}")
return client_map
def ensure_employees(cur, branch_map, role, catalog, existing_names=None):
default_branch = get_default_branch(cur)
emp_map = {}
if existing_names is None:
cur.execute("SELECT id, name FROM employees")
existing_names = {name.strip().lower(): id for id, name in cur.fetchall()}
for e in catalog:
name = (e.get('nombre') or '').strip()
if not name:
continue
key = name.lower()
if key in existing_names:
emp_map[e['id']] = existing_names[key]
continue
is_active = bool(e.get('activo', True))
cur.execute(
"""
INSERT INTO employees (name, role, branch_id, is_active)
VALUES (%s, %s, %s, %s) RETURNING id
""",
(name, role, default_branch, is_active)
)
new_id = cur.fetchone()[0]
existing_names[key] = new_id
emp_map[e['id']] = new_id
print(f"Created employee {role} {e['id']}: {name}")
return emp_map
def ensure_couriers(cur, catalog, tenant_id=31):
cur.execute("SELECT id, name FROM couriers WHERE tenant_id = %s", (tenant_id,))
existing = {name.strip().lower(): id for id, name in cur.fetchall()}
courier_map = {}
for c in catalog:
name = (c.get('nombre') or '').strip()
if not name:
continue
key = name.lower()
if key in existing:
courier_map[c['id']] = existing[key]
continue
cur.execute(
"INSERT INTO couriers (tenant_id, name, code, is_active) VALUES (%s, %s, %s, true) RETURNING id",
(tenant_id, name, f"MOT-{c['id']}")
)
new_id = cur.fetchone()[0]
existing[key] = new_id
courier_map[c['id']] = new_id
print(f"Created courier {c['id']}: {name}")
return courier_map
def ensure_articles(cur, branch_map):
"""Rached articles are kept as free-text lines, not inventory products."""
catalog = load_json('catalog_articulos')
article_map = {}
for a in catalog:
name = (a.get('nombre') or '').strip()
if name:
article_map[a['id']] = name
return article_map
def ensure_users(cur, branch_map, order_details):
# Map Rached user ids to employees
default_branch = get_default_branch(cur)
cur.execute("SELECT id, name FROM employees")
existing = {name.strip().lower(): id for id, name in cur.fetchall()}
user_map = {}
for d in order_details:
datos = d.get('datos', {})
uid = datos.get('idUsuario')
name = (datos.get('usuarioNombre') or '').strip()
if not uid or not name:
continue
key = name.lower()
if key in existing:
user_map[uid] = existing[key]
continue
cur.execute(
"INSERT INTO employees (name, role, branch_id, is_active) VALUES (%s, %s, %s, true) RETURNING id",
(name, 'workshop', default_branch)
)
new_id = cur.fetchone()[0]
existing[key] = new_id
user_map[uid] = new_id
print(f"Created user {uid}: {name}")
return user_map
def import_orders(cur, branch_map, client_map, mech_map, courier_map, article_map, user_map):
order_files = sorted(DATA_DIR.glob('details/order_*.json'))
imported = 0
skipped = 0
for f in order_files:
d = json.loads(f.read_text(encoding='utf-8'))
datos = d.get('datos', {})
order_number = datos.get('numero')
if not order_number:
continue
cur.execute("SELECT id FROM service_orders WHERE order_number = %s", (order_number,))
if cur.fetchone():
skipped += 1
continue
branch_id = branch_map.get(datos.get('idSucursal'))
customer_id = client_map.get(datos.get('idCliente'))
status = ORDER_STATUS_MAP.get(datos.get('idEstatus'), 'por_revisar')
# delivery method
via = datos.get('idViaEntrega')
mot_rec = datos.get('idMotociclistaRecoleccion')
mot_ent = datos.get('idMotociclistaEntrega')
courier_id = None
if via == 1:
delivery_method = 'pickup'
elif via == 2:
if mot_ent:
delivery_method = 'courier'
courier_id = courier_map.get(mot_ent)
elif mot_rec:
delivery_method = 'courier'
courier_id = courier_map.get(mot_rec)
else:
delivery_method = 'delivery'
else:
delivery_method = None
# customer address/phone from order overrides if present? order detail only has ids.
# Prefill from customer record (already stored in customers)
workshop_name = None
customer_phone = None
customer_address = None
if customer_id:
cur.execute("SELECT name, phone, address FROM customers WHERE id = %s", (customer_id,))
row = cur.fetchone()
if row:
workshop_name, customer_phone, customer_address = row
created_by = user_map.get(datos.get('idUsuario'))
fecha = datos.get('fecha')
created_at = datetime.strptime(fecha, '%Y-%m-%d %H:%M:%S') if fecha else datetime.now()
cur.execute(
"""
INSERT INTO service_orders
(tenant_id, branch_id, customer_id, order_number, status,
workshop_name, customer_address, customer_phone, vehicle_description,
reception_notes, estimated_cost, final_cost,
delivery_method, courier_id, is_direct, requires_invoice,
created_by, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""",
(
31, # tenant_id for Rached
branch_id,
customer_id,
order_number,
status,
workshop_name,
customer_address,
customer_phone,
datos.get('vehiculo'),
datos.get('observaciones'),
datos.get('presupuesto') or 0,
datos.get('total') or 0,
delivery_method,
courier_id,
bool(datos.get('ordenDirecta')),
bool(datos.get('requiereFactura')),
created_by,
created_at,
created_at,
)
)
so_id = cur.fetchone()[0]
# status history
cur.execute(
"""
INSERT INTO service_order_status_history (service_order_id, new_status, changed_by, notes, created_at)
VALUES (%s, %s, %s, %s, %s)
""",
(so_id, status, created_by, 'Importado desde app Rached', created_at)
)
# items (free-text lines; not linked to inventory)
for it in datos.get('detalles', []):
articulo = it.get('articulo', {})
art_id = articulo.get('id')
art_name = article_map.get(art_id) or articulo.get('nombre') or 'Concepto'
mech_id = mech_map.get(it.get('idMecanico'))
item_status = ITEM_STATUS_MAP.get(it.get('idEstatusDetalle'), 'por_revisar')
qty = it.get('cantidad', 1)
price = it.get('precio') or 0
cur.execute(
"""
INSERT INTO service_order_items
(service_order_id, inventory_id, part_number, name, quantity,
unit_cost, unit_price, status, mechanic_id, observations)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
so_id,
None,
None,
art_name,
qty,
price,
price,
item_status,
mech_id,
it.get('observaciones'),
)
)
imported += 1
if imported % 100 == 0:
print(f"Imported {imported} orders...")
return imported, skipped
def main():
print(f"Importing Rached workshop data into {TENANT_DB}")
conn = connect()
cur = conn.cursor()
try:
branch_map = ensure_branches(cur)
print(f"Branch map: {branch_map}")
client_map = ensure_customers(cur, branch_map)
print(f"Customers to import: {len(client_map)}")
mech_catalog = load_json('catalog_mecanicos')
mech_map = ensure_employees(cur, branch_map, 'mechanic', mech_catalog)
print(f"Mechanics map: {len(mech_map)}")
courier_catalog = load_json('catalog_motociclistas')
courier_map = ensure_couriers(cur, courier_catalog, tenant_id=31)
print(f"Couriers map: {len(courier_map)}")
article_map = ensure_articles(cur, branch_map)
print(f"Articles map: {len(article_map)}")
order_details = [json.loads(f.read_text(encoding='utf-8')) for f in DATA_DIR.glob('details/order_*.json')]
user_map = ensure_users(cur, branch_map, order_details)
print(f"Users map: {len(user_map)}")
imported, skipped = import_orders(cur, branch_map, client_map, mech_map, courier_map, article_map, user_map)
print(f"Imported: {imported}, Skipped (already exist): {skipped}")
conn.commit()
except Exception as e:
conn.rollback()
print(f"ERROR: {e}")
raise
finally:
cur.close()
conn.close()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,32 @@
const { chromium } = require('playwright');
const fs = require('fs');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://app.flechasyventiladores-rached.com/', { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
await page.evaluate(() => {
document.getElementById('correo').removeAttribute('pattern');
document.getElementById('password').removeAttribute('pattern');
});
await page.fill('#correo', 'IVAN@flechasyventiladores-rached.com');
await page.fill('#password', 'Nexus01');
await page.evaluate(() => {
['correo','password'].forEach(id => {
const el = document.getElementById(id);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
});
});
await page.waitForTimeout(1000);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/login_filled.png', fullPage: true });
fs.writeFileSync('/home/Autopartes/data/rached_import/login_filled.html', await page.content());
// click enabled submit button if any
await page.click('button[color="primary"]');
await page.waitForTimeout(8000);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/after_login.png', fullPage: true });
fs.writeFileSync('/home/Autopartes/data/rached_import/after_login.html', await page.content());
console.log('done');
await browser.close();
})();

View File

@@ -0,0 +1,14 @@
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://app.flechasyventiladores-rached.com/', { waitUntil: 'networkidle' });
await page.waitForTimeout(5000);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/login.png', fullPage: true });
fs.writeFileSync('/home/Autopartes/data/rached_import/login.html', await page.content());
console.log('done');
await browser.close();
})();

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""Scrape Rached legacy catalog endpoints discovered via UI."""
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'))
USER = 'IVAN@flechasyventiladores-rached.com'
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 (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': USER,
'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_get(token, path, params=None):
url = f'{BASE_API}{path}'
resp = requests.get(url, headers=api_headers(token), params=params, timeout=30)
resp.raise_for_status()
return resp.json()
def fetch_post(token, path, payload=None, params=None):
url = f'{BASE_API}{path}'
resp = requests.post(url, headers=api_headers(token), json=payload or {}, params=params, timeout=30)
resp.raise_for_status()
return resp.json()
def save(name, data):
path = OUT_DIR / f'{name}.json'
path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding='utf-8')
return path
def fetch_list_all(token, path, method='GET', payload=None):
"""Fetch a list endpoint that may return either a list or Laravel pagination."""
all_items = []
page = 1
while True:
try:
if method == 'GET':
data = fetch_get(token, f'{path}?page={page}')
else:
data = fetch_post(token, f'{path}?page={page}', payload)
except Exception as e:
print(f' {path} page {page} error: {e}')
break
datos = data.get('datos', {})
if isinstance(datos, list):
# Non-paginated full list
all_items.extend(datos)
print(f' {path}: +{len(datos)} (total {len(all_items)})')
break
items = datos.get('data', [])
all_items.extend(items)
print(f' {path} page {page}: +{len(items)} (total {len(all_items)}/{datos.get("total", "?")})')
if datos.get('current_page', 1) >= datos.get('last_page', 1):
break
page += 1
time.sleep(0.6)
return all_items
def main():
OUT_DIR.mkdir(parents=True, exist_ok=True)
log_path = OUT_DIR / 'scrape_catalogs.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.')
# Simple catalog endpoints (GET, single page or paginated)
simple_gets = {
'catalog_sucursales': '/api/catalogos/sucursales',
'catalog_viasentrega': '/api/catalogos/viasentrega/listado',
'catalog_order_statuses': '/api/catalogos/ordenesservicioestatus/listado',
'catalog_item_statuses': '/api/catalogos/ordenesserviciodetalleestatus/listado',
'catalog_directas': '/api/catalogos/ordenesDirectas',
'catalog_telefonostipos': '/api/catalogos/telefonostipos/listado',
'catalog_direccionestipos': '/api/catalogos/direccionestipos/listado',
}
for name, path in simple_gets.items():
try:
data = fetch_get(token, path)
save(name, data)
count = len(data.get('datos', []) if isinstance(data.get('datos'), list) else [])
log(f'{name}: {count} items')
except Exception as e:
log(f'{name} error: {e}')
time.sleep(0.6)
# List endpoints (GET or POST)
list_gets = {
'catalog_articulos': '/api/catalogos/articulos/listado',
'catalog_mecanicos': '/api/catalogos/mecanicos/listado',
'catalog_motociclistas': '/api/catalogos/motociclistas/listado',
}
for name, path in list_gets.items():
items = fetch_list_all(token, path, method='GET')
save(name, items)
log(f'{name}: {len(items)} items')
# Client list
try:
client_items = fetch_list_all(token, '/api/catalogos/clientes/listado', method='POST',
payload={'taller': '', 'nombreCompleto': '', 'idClienteEstatus': '-1'})
save('catalog_clientes', client_items)
log(f'catalog_clientes: {len(client_items)} items')
# Fetch full client details for clients referenced in orders
orders_path = OUT_DIR / 'orders_list.json'
if orders_path.exists():
order_ids = {o['idCliente'] for o in json.loads(orders_path.read_text(encoding='utf-8')) if o.get('idCliente')}
log(f'Fetching details for {len(order_ids)} unique order clients...')
client_details = {}
for idx, cid in enumerate(sorted(order_ids), 1):
detail_path = OUT_DIR / 'client_details' / f'client_{cid}.json'
if detail_path.exists():
client_details[cid] = json.loads(detail_path.read_text(encoding='utf-8'))
continue
try:
data = fetch_get(token, f'/api/catalogos/clientes/{cid}')
client_details[cid] = data.get('datos')
detail_path.parent.mkdir(parents=True, exist_ok=True)
detail_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding='utf-8')
except Exception as e:
log(f' client {cid} detail error: {e}')
if idx % 30 == 0:
time.sleep(1)
else:
time.sleep(0.2)
save('catalog_client_details', client_details)
log(f'catalog_client_details: {len(client_details)} items')
except Exception as e:
log(f'catalog_clientes error: {e}')
log('Done.')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Fetch full client details for clients referenced in Rached service orders."""
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):
url = f'{BASE_API}/api/catalogos/clientes/{cid}'
resp = requests.get(url, headers={**HEADERS, 'Authorization': f'Bearer {token}'}, timeout=30)
resp.raise_for_status()
return resp.json()
def main():
OUT_DIR.mkdir(parents=True, exist_ok=True)
detail_dir = OUT_DIR / 'client_details'
detail_dir.mkdir(exist_ok=True)
# collect unique client ids from order details
client_ids = 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:
client_ids.add(cid)
print(f'Found {len(client_ids)} unique clients from order details')
token = login()
print('Logged in')
results = {}
for idx, cid in enumerate(sorted(client_ids), 1):
out_path = detail_dir / f'client_{cid}.json'
if out_path.exists():
data = json.loads(out_path.read_text(encoding='utf-8'))
else:
try:
data = fetch_client(token, cid)
except Exception as e:
print(f'[{idx}/{len(client_ids)}] client {cid} error: {e}')
continue
out_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding='utf-8')
results[cid] = data.get('datos')
print(f'[{idx}/{len(client_ids)}] client {cid} saved')
time.sleep(0.4 if idx % 30 else 1.5)
summary_path = OUT_DIR / 'catalog_client_details.json'
summary_path.write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding='utf-8')
print(f'Saved {len(results)} client details to {summary_path}')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,91 @@
#!/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()

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env node
/**
* Scrape service orders from Rached's legacy web app.
* Run with:
* RACHED_USER='IVAN' RACHED_PASS='Nexus01' node scripts/scrape_rached_workshop.js
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const OUT_DIR = process.env.OUT_DIR || path.join(__dirname, '..', 'data', 'rached_import');
const USER = process.env.RACHED_USER || 'IVAN';
const PASS = process.env.RACHED_PASS || 'Nexus01';
const BASE_APP = 'http://app.flechasyventiladores-rached.com';
const BASE_API = 'http://appapi.flechasyventiladores-rached.com';
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function main() {
fs.mkdirSync(OUT_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1440, height: 900 }
});
const page = await context.newPage();
// ── LOGIN ──
console.log('Logging in...');
await page.goto(`${BASE_APP}/login`);
await page.waitForSelector('#correo', { timeout: 10000 });
await page.fill('#correo', USER);
await page.fill('#password', PASS);
await page.click('button:has-text("Aceptar")');
await page.waitForURL('**/administracion/**', { timeout: 20000 });
console.log('Logged in.');
// ── ORDERS LIST ──
console.log('Navigating to orders list...');
const allOrders = [];
let pageNum = 1;
let respPromise = page.waitForResponse(
r => r.url().includes('/api/servicios/ordenes/listado') && r.status() === 200,
{ timeout: 30000 }
);
await page.goto(`${BASE_APP}/administracion/servicios/ordenesservicio`);
while (true) {
if (pageNum > 1) {
const nextBtn = page.locator('button.mat-paginator-navigation-next');
const isDisabled = await nextBtn.evaluate(el => el.disabled).catch(() => true);
if (isDisabled) {
console.log('No more pages.');
break;
}
respPromise = page.waitForResponse(
r => r.url().includes('/api/servicios/ordenes/listado') && r.status() === 200,
{ timeout: 30000 }
);
await nextBtn.click();
}
const resp = await respPromise;
const payload = await resp.json();
const datos = payload.datos || {};
const orders = datos.data || [];
allOrders.push(...orders);
fs.writeFileSync(
path.join(OUT_DIR, `orders_page_${pageNum}.json`),
JSON.stringify(payload, null, 2)
);
console.log(`Page ${pageNum}: ${orders.length} orders (total ${allOrders.length}/${datos.total || '?'})`);
if (datos.current_page >= datos.last_page) break;
pageNum++;
await sleep(800);
}
fs.writeFileSync(
path.join(OUT_DIR, 'orders_list.json'),
JSON.stringify(allOrders, null, 2)
);
console.log(`Collected ${allOrders.length} orders summary.`);
await page.waitForSelector('mat-row', { timeout: 10000 });
await sleep(3000);
if (!allOrders.length) {
await browser.close();
return;
}
// ── ORDER DETAILS ──
const detailDir = path.join(OUT_DIR, 'details');
fs.mkdirSync(detailDir, { recursive: true });
let targetId = null;
await page.route('**/api/servicios/ordenes/*', async (route, request) => {
if (targetId && request.url().match(/\/ordenes\/\d+$/)) {
const url = new URL(request.url());
const parts = url.pathname.split('/');
parts[parts.length - 1] = String(targetId);
url.pathname = parts.join('/');
await route.continue({ url: url.toString() });
return;
}
await route.continue();
});
async function clickFirstEditButton() {
await page.evaluate(() => {
const row = document.querySelector('mat-row');
if (!row) return;
const btn = row.querySelector('button.mat-icon-button i.fa-edit');
if (btn) btn.closest('button').click();
});
}
const limit = parseInt(process.env.LIMIT || '0', 10);
const ordersToFetch = limit > 0 ? allOrders.slice(0, limit) : allOrders;
for (let i = 0; i < ordersToFetch.length; i++) {
const o = ordersToFetch[i];
targetId = o.id;
const detailPath = path.join(detailDir, `order_${targetId}.json`);
if (fs.existsSync(detailPath)) {
process.stdout.write(`\rDetail ${i + 1}/${ordersToFetch.length}: ${targetId} (already saved) `);
continue;
}
let attempts = 0;
while (attempts < 3) {
attempts++;
try {
const respPromise = page.waitForResponse(
r => r.url().includes(`/api/servicios/ordenes/${targetId}`) && r.status() === 200,
{ timeout: 20000 }
);
await clickFirstEditButton();
const resp = await respPromise;
const detail = await resp.json();
fs.writeFileSync(
path.join(detailDir, `order_${targetId}.json`),
JSON.stringify(detail, null, 2)
);
process.stdout.write(`\rDetail ${i + 1}/${ordersToFetch.length}: ${targetId} `);
// close modal if opened
await page.keyboard.press('Escape').catch(() => {});
await sleep(300);
break;
} catch (e) {
console.error(`\nError fetching detail for ${targetId} (attempt ${attempts}):`, e.message);
await page.keyboard.press('Escape').catch(() => {});
await sleep(1000);
}
}
}
console.log('\nDetail extraction complete.');
await browser.close();
console.log(`Output saved to ${OUT_DIR}`);
}
main().catch(e => {
console.error(e);
process.exit(1);
});

View 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()

View File

@@ -0,0 +1,25 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: 'http://localhost:5001', serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
page.on('pageerror', e => errors.push('PAGE: ' + e.message));
page.on('console', msg => { if (msg.type() === 'error') errors.push('CONSOLE: ' + msg.text()); });
page.on('response', res => { if (res.status() >= 400) errors.push(`HTTP ${res.status()}: ${res.url()}`); });
await page.goto('/pos/login2?tenant=31');
await page.waitForSelector('.user-avatar-btn', { timeout: 10000 });
await page.click('.user-avatar-btn[data-id="33"]');
for (const digit of '1234') await page.click(`.pin-key[data-digit="${digit}"]`);
await page.click('#btn-login');
await page.waitForURL('**/pos/catalog', { timeout: 10000 });
await page.goto('/pos/sale');
await page.waitForTimeout(1500);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/cashier_pos_default.png', fullPage: true });
console.log('Errors:', errors.length);
errors.forEach(e => console.log(' -', e));
await browser.close();
})();

View File

@@ -0,0 +1,37 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: 'http://localhost:5001', serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
page.on('pageerror', e => errors.push('PAGE: ' + e.message));
page.on('console', msg => { if (msg.type() === 'error') errors.push('CONSOLE: ' + msg.text()); });
page.on('response', res => { if (res.status() >= 400) errors.push(`HTTP ${res.status()}: ${res.url()}`); });
await page.goto('/pos/login2?tenant=31');
await page.waitForSelector('.user-avatar-btn', { timeout: 10000 });
await page.click('.user-avatar-btn[data-id="32"]');
for (const digit of '1234') await page.click(`.pin-key[data-digit="${digit}"]`);
await page.click('#btn-login');
await page.waitForURL('**/pos/workshop', { timeout: 10000 });
await page.goto('/pos/catalog');
await page.waitForTimeout(1500);
const info = await page.evaluate(() => ({
title: document.title,
bodyClass: document.body.className,
scripts: Array.from(document.querySelectorAll('script[src]')).map(s => s.src).filter(s => s.includes('sidebar') || s.includes('app-init'))
}));
console.log('Page info:', JSON.stringify(info, null, 2));
await page.screenshot({ path: '/home/Autopartes/data/rached_import/counter_sidebar_catalog.png', fullPage: true });
// Try dashboard
await page.goto('/pos/dashboard');
await page.waitForTimeout(1500);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/counter_dashboard_redirect.png', fullPage: true });
console.log('URL after dashboard:', page.url());
console.log('Errors:', errors.length);
errors.forEach(e => console.log(' -', e));
await browser.close();
})();

View File

@@ -0,0 +1,31 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: 'http://localhost:5001', serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
page.on('pageerror', e => errors.push('PAGE: ' + e.message));
page.on('console', msg => { if (msg.type() === 'error') errors.push('CONSOLE: ' + msg.text()); });
page.on('response', res => { if (res.status() >= 400) errors.push(`HTTP ${res.status()}: ${res.url()}`); });
await page.goto('/pos/login2?tenant=31');
await page.waitForSelector('.user-avatar-btn', { timeout: 10000 });
await page.click('.user-avatar-btn[data-id="1"]');
for (const digit of '1234') await page.click(`.pin-key[data-digit="${digit}"]`);
await page.click('#btn-login');
await page.waitForURL('**/pos/catalog', { timeout: 10000 });
await page.goto('/pos/inventory');
await page.waitForTimeout(1500);
await page.click('button:has-text("Entradas")');
await page.waitForTimeout(500);
await page.click('button:has-text("Nueva Entrada")');
await page.waitForTimeout(500);
await page.fill('#purchaseItemSearch', 'CVBT2170');
await page.waitForTimeout(800);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/inventory_entrada_alias.png', fullPage: true });
console.log('Errors:', errors.length);
errors.forEach(e => console.log(' -', e));
await browser.close();
})();

View File

@@ -0,0 +1,25 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: 'http://localhost:5001', serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
page.on('pageerror', e => errors.push('PAGE: ' + e.message));
page.on('console', msg => { if (msg.type() === 'error') errors.push('CONSOLE: ' + msg.text()); });
page.on('response', res => { if (res.status() >= 400) errors.push(`HTTP ${res.status()}: ${res.url()}`); });
await page.goto('/pos/login2?tenant=31');
await page.waitForSelector('.user-avatar-btn', { timeout: 10000 });
await page.click('.user-avatar-btn[data-id="31"]');
for (const digit of '1234') await page.click(`.pin-key[data-digit="${digit}"]`);
await page.click('#btn-login');
await page.waitForURL('**/pos/workshop', { timeout: 10000 });
await page.goto('/pos/sale');
await page.waitForTimeout(2000);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/pos_counter_debug.png', fullPage: true });
console.log('URL:', page.url());
console.log('Errors:', errors.length);
errors.forEach(e => console.log(' -', e));
await browser.close();
})();

View File

@@ -0,0 +1,28 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: 'http://localhost:5001', serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
page.on('pageerror', e => errors.push('PAGE: ' + e.message));
page.on('console', msg => { if (msg.type() === 'error') errors.push('CONSOLE: ' + msg.text()); });
page.on('response', res => { if (res.status() >= 400) errors.push(`HTTP ${res.status()}: ${res.url()}`); });
await page.goto('/pos/login2?tenant=31');
await page.waitForSelector('.user-avatar-btn', { timeout: 10000 });
await page.click('.user-avatar-btn[data-id="1"]');
for (const digit of '1234') await page.click(`.pin-key[data-digit="${digit}"]`);
await page.click('#btn-login');
await page.waitForURL('**/pos/catalog', { timeout: 10000 });
await page.goto('/pos/reports');
await page.waitForTimeout(1500);
await page.click('button:has-text("Cortes de caja")');
await page.waitForTimeout(1500);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/reports_cortes_tab.png', fullPage: true });
console.log('Errors:', errors.length);
errors.forEach(e => console.log(' -', e));
await browser.close();
})();

View File

@@ -0,0 +1,35 @@
const { chromium } = require('playwright');
const fs = require('fs');
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: 'http://localhost:5001', serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
page.on('pageerror', e => errors.push('PAGE: ' + e.message));
page.on('console', msg => { errors.push('CONSOLE(' + msg.type() + '): ' + msg.text()); });
page.on('response', res => { if (res.status() >= 400) errors.push(`HTTP ${res.status()}: ${res.url()}`); });
await page.goto('/pos/login2?tenant=31');
await page.waitForSelector('.user-avatar-btn', { timeout: 10000 });
await page.click('.user-avatar-btn[data-id="29"]');
for (const digit of '1234') await page.click(`.pin-key[data-digit="${digit}"]`);
await page.click('#btn-login');
await page.waitForURL('**/pos/workshop', { timeout: 10000 });
await page.waitForTimeout(1500);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/workshop_counter_list.png', fullPage: true });
await page.click('button:has-text("Ver")');
await page.waitForTimeout(500);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/workshop_counter_detail.png', fullPage: true });
await page.click('button:has-text("Cerrar")');
await page.waitForTimeout(300);
await page.click('#btnNewOrder');
await page.waitForTimeout(800);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/workshop_counter_new.png', fullPage: true });
console.log('Errors:', errors.length);
errors.forEach(e => console.log(' -', e));
await browser.close();
})();

View File

@@ -0,0 +1,34 @@
const { chromium } = require('playwright');
const fs = require('fs');
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: 'http://localhost:5001', serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
page.on('pageerror', e => errors.push('PAGE: ' + e.message));
page.on('console', msg => { if (msg.type() === 'error') errors.push('CONSOLE: ' + msg.text()); });
page.on('response', res => { if (res.status() >= 400) errors.push(`HTTP ${res.status()}: ${res.url()}`); });
await page.goto('/pos/login2?tenant=31');
await page.waitForSelector('.user-avatar-btn', { timeout: 10000 });
await page.click('.user-avatar-btn[data-id="2"]');
for (const digit of '1234') await page.click(`.pin-key[data-digit="${digit}"]`);
await page.click('#btn-login');
await page.waitForURL('**/pos/workshop', { timeout: 10000 });
await page.waitForTimeout(1500);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/workshop_restricted_list.png', fullPage: true });
await page.click('button:has-text("Ver")');
await page.waitForTimeout(500);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/workshop_restricted_detail.png', fullPage: true });
await page.click('button:has-text("Artículos"), .so-tabs__btn:has-text("Artículos")');
await page.waitForTimeout(500);
await page.screenshot({ path: '/home/Autopartes/data/rached_import/workshop_restricted_articles.png', fullPage: true });
console.log('Errors:', errors.length);
errors.forEach(e => console.log(' -', e));
await browser.close();
})();