clientGBP 2.2.0__tar.gz → 2.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clientgbp-2.2.0 → clientgbp-2.2.2}/PKG-INFO +1 -1
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/__init__.py +3 -0
- clientgbp-2.2.2/gbp/parsers/customers.py +35 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/ws.py +3 -3
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/base.py +60 -30
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/customers.py +3 -6
- {clientgbp-2.2.0 → clientgbp-2.2.2}/pyproject.toml +1 -1
- {clientgbp-2.2.0 → clientgbp-2.2.2}/LICENSE +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/README.md +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/__init__.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/client.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/decorator/__init__.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/decorator/require_login.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/domain/__init__.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/domain/enums.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/domain/models.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/http/__init__.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/http/headers.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/http/http.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/http/urls.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/entities.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/forms.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/html.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/items.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/orders.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/__init__.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/_constants.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/_form_utils.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/_viewstate_utils.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/auth.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/contacts.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/customers.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/orders.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/__init__.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/auth.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/contacts.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/gbp_service.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/orders.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/reports.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/validators/__init__.py +0 -0
- {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/validators/identifiers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clientGBP
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Libreria para el uso de GBP mediante HTTP. Sin utilizar interfaz gráfica (Scraping).Usando las credenciales de una cuenta, se obtiene las cookies de sesión y se pueden realizar operaciones. Se realiza todo de manera sincrona.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Paquete de parsers HTML/AJAX y extractores de formularios."""
|
|
2
2
|
|
|
3
|
+
from gbp.parsers.customers import extract_client_edit_keys, extract_usd_client_ids
|
|
3
4
|
from gbp.parsers.entities import parse_client_form, parse_contact_form
|
|
4
5
|
from gbp.parsers.forms import (
|
|
5
6
|
extract_all_form_fields,
|
|
@@ -32,10 +33,12 @@ __all__ = [
|
|
|
32
33
|
"extract_ajax_tokens",
|
|
33
34
|
"extract_order_detail_dialog_url",
|
|
34
35
|
"extract_all_form_fields",
|
|
36
|
+
"extract_client_edit_keys",
|
|
35
37
|
"extract_item_code_from_row",
|
|
36
38
|
"extract_order_message",
|
|
37
39
|
"extract_save_order_alert",
|
|
38
40
|
"extract_sku_tail",
|
|
41
|
+
"extract_usd_client_ids",
|
|
39
42
|
"get_input_value",
|
|
40
43
|
"get_option_value_by_text",
|
|
41
44
|
"get_select_text",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Parsers de la grilla de resultados de la búsqueda de clientes (`table#grdData`)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
|
|
7
|
+
# Un CUIT puede tener dos fichas: la real, en dólares, y una espejo en pesos que crea Mercado Libre.
|
|
8
|
+
USD_ACCOUNT = "(USD)"
|
|
9
|
+
EDIT_LINK_PATTERN = re.compile(r"wfmGenericEditNew\.aspx','(\?[^']+)'")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_client_edit_keys(soup: BeautifulSoup) -> list[str]:
|
|
13
|
+
"""Extrae la clave del link "Modificar Registro" de cada fila, primero las fichas en dólares.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
soup (BeautifulSoup): Grilla de resultados parseada.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
list[str]: Claves de edición, una por cliente.
|
|
20
|
+
"""
|
|
21
|
+
rows = sorted(soup.select("table#grdData tr"), key=lambda row: USD_ACCOUNT not in row.get_text())
|
|
22
|
+
return [match.group(1) for row in rows if (match := EDIT_LINK_PATTERN.search(str(row)))]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_usd_client_ids(soup: BeautifulSoup) -> set[int]:
|
|
26
|
+
"""Extrae los IDs (última columna) de las fichas con cuenta corriente en dólares.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
soup (BeautifulSoup): Grilla de resultados parseada.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
set[int]: IDs de cliente de las filas en dólares.
|
|
33
|
+
"""
|
|
34
|
+
ids = {row.find_all("td")[-1].get_text(strip=True) for row in soup.select("table#grdData tr") if USD_ACCOUNT in row.get_text()}
|
|
35
|
+
return {int(client_id) for client_id in ids if client_id.isdigit()}
|
|
@@ -4,18 +4,18 @@ import re
|
|
|
4
4
|
|
|
5
5
|
from gbp.validators.identifiers import get_national_id
|
|
6
6
|
|
|
7
|
-
def crm_payload(tax_number: str) -> dict[str, str | int]:
|
|
7
|
+
def crm_payload(tax_number: str, prefix_text: str | None = None) -> dict[str, str | int]:
|
|
8
8
|
"""Obtener payload para la búsqueda del CRM completo de un cliente para crear un contacto.
|
|
9
9
|
|
|
10
10
|
Args:
|
|
11
11
|
tax_number (str): CUIT o DNI del cliente objetivo.
|
|
12
|
+
prefix_text (str | None): Texto a buscar; por defecto el DNI extraido de `tax_number`.
|
|
12
13
|
|
|
13
14
|
Returns:
|
|
14
15
|
dict[str, str | int]: Payload serializable para enviar al endpoint correspondiente.
|
|
15
16
|
"""
|
|
16
|
-
national_id = get_national_id(tax_number)
|
|
17
17
|
return {
|
|
18
|
-
"prefixText":
|
|
18
|
+
"prefixText": prefix_text or get_national_id(tax_number),
|
|
19
19
|
"count": 20,
|
|
20
20
|
"contextKey": "2/1/1/50031/1/-1/-1/2/-1",
|
|
21
21
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Base service con estado de sesion y utilidades HTTP/comunes de GBP."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import re
|
|
5
4
|
from typing import Any
|
|
6
5
|
|
|
7
6
|
from bs4 import BeautifulSoup
|
|
@@ -9,12 +8,19 @@ from bs4.element import Tag
|
|
|
9
8
|
from requests import RequestException, Response, Session
|
|
10
9
|
|
|
11
10
|
from gbp.decorator.require_login import require_login
|
|
12
|
-
from gbp.http.headers import BASE_HEADERS, HEADERS_JSON_CONTACT
|
|
11
|
+
from gbp.http.headers import BASE_HEADERS, HEADERS_AJAX, HEADERS_JSON_CONTACT
|
|
13
12
|
from gbp.http.http import HttpClient
|
|
14
|
-
from gbp.http.urls import BASE_URL, CUSTOMER_WS_URL, DEFAULT_TIMEOUT, ITEM_WS_URL, MAIN_URL
|
|
15
|
-
from gbp.parsers import
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
from gbp.http.urls import BASE_URL, CUSTOMER_WS_URL, DEFAULT_TIMEOUT, ITEM_WS_URL, MAIN_URL, SEARCH_URL
|
|
14
|
+
from gbp.parsers import (
|
|
15
|
+
extract_client_edit_keys,
|
|
16
|
+
extract_sku_tail,
|
|
17
|
+
extract_usd_client_ids,
|
|
18
|
+
get_input_value,
|
|
19
|
+
match_item_to_requested_sku,
|
|
20
|
+
parse_html,
|
|
21
|
+
)
|
|
22
|
+
from gbp.payloads import crm_payload, item_search_payload, search_client_payload
|
|
23
|
+
from gbp.validators import get_national_id, normalize_valid_tax_number
|
|
18
24
|
|
|
19
25
|
logger = logging.getLogger(__name__)
|
|
20
26
|
logger.addHandler(logging.NullHandler())
|
|
@@ -217,7 +223,7 @@ class BaseGBPService:
|
|
|
217
223
|
return "grdData" in html
|
|
218
224
|
|
|
219
225
|
def _extract_client_edit_url(self, html: str) -> str | None:
|
|
220
|
-
"""Extrae la URL de edición de un cliente desde los resultados de búsqueda.
|
|
226
|
+
"""Extrae la URL de edición de un cliente desde los resultados de búsqueda (la ficha en dólares si hay).
|
|
221
227
|
|
|
222
228
|
Args:
|
|
223
229
|
html (str): Contenido HTML a analizar.
|
|
@@ -225,11 +231,9 @@ class BaseGBPService:
|
|
|
225
231
|
Returns:
|
|
226
232
|
str | None: Cadena encontrada o `None` cuando el dato no esta disponible.
|
|
227
233
|
"""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
edit_key = matches[0]
|
|
232
|
-
return f"{BASE_URL}/wfmGenericEditNew.aspx{edit_key}"
|
|
234
|
+
edit_keys = extract_client_edit_keys(parse_html(html))
|
|
235
|
+
if edit_keys:
|
|
236
|
+
return f"{BASE_URL}/wfmGenericEditNew.aspx{edit_keys[0]}"
|
|
233
237
|
return None
|
|
234
238
|
|
|
235
239
|
def _get_header_page_details(self) -> tuple[BeautifulSoup, str]:
|
|
@@ -277,33 +281,59 @@ class BaseGBPService:
|
|
|
277
281
|
logger.warning("CUIT/DNI invalido para busqueda CRM: %s", tax_number)
|
|
278
282
|
return None
|
|
279
283
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
logger.error(f"Error en búsqueda CRM. Status: {resp_customer.status_code}")
|
|
285
|
-
return None
|
|
284
|
+
# GBP solo matchea por inicio de palabra: un CUIT guardado sin guiones no aparece buscando por DNI.
|
|
285
|
+
prefixes = [get_national_id(normalized_tax_number)]
|
|
286
|
+
if len(normalized_tax_number) == 11:
|
|
287
|
+
prefixes.append(normalized_tax_number)
|
|
286
288
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
clients_crm =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
289
|
+
try:
|
|
290
|
+
# Se buscan siempre los dos: la ficha en pesos y la de dólares pueden guardar el CUIT distinto.
|
|
291
|
+
clients_crm: list[str] = []
|
|
292
|
+
for prefix_text in prefixes:
|
|
293
|
+
resp_customer = self._send_post_json(
|
|
294
|
+
CUSTOMER_WS_URL, crm_payload(normalized_tax_number, prefix_text), HEADERS_JSON_CONTACT
|
|
295
|
+
)
|
|
296
|
+
logger.debug(resp_customer.text)
|
|
297
|
+
if resp_customer.status_code != 200:
|
|
298
|
+
logger.error(f"Error en búsqueda CRM. Status: {resp_customer.status_code}")
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
# Obtiene los resultados encontrados y lo parsea a JSON para manejarlo de mejor manera.
|
|
302
|
+
found = resp_customer.json()["d"]
|
|
303
|
+
logger.info(f"Clientes encontrados para {prefix_text}: {found}")
|
|
304
|
+
clients_crm += found
|
|
305
|
+
|
|
306
|
+
if not clients_crm:
|
|
293
307
|
logger.warning("No se encontró cliente con ese CUIT")
|
|
294
308
|
return None
|
|
295
309
|
|
|
296
|
-
if
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return clients_crm[0]
|
|
310
|
+
if len(clients_crm) > 1:
|
|
311
|
+
# El CRM ("RAZÓN SOCIAL |00006149|CUIT") no trae la moneda: se cruza por ID con la grilla.
|
|
312
|
+
usd_ids = self._get_usd_client_ids(normalized_tax_number)
|
|
313
|
+
usd_crm_clients = [client for client in clients_crm if any(f"|{client_id:08d}|" in client for client_id in usd_ids)]
|
|
314
|
+
return (usd_crm_clients or [client for client in clients_crm if "PESOS" not in client] or clients_crm)[0]
|
|
301
315
|
|
|
302
|
-
return
|
|
316
|
+
return clients_crm[0]
|
|
303
317
|
except Exception as e:
|
|
304
318
|
logger.error(f"Error inesperado buscando en CRM: {e}")
|
|
305
319
|
return None
|
|
306
320
|
|
|
321
|
+
def _get_usd_client_ids(self, tax_number: str) -> set[int]:
|
|
322
|
+
"""Busca el CUIT en la grilla de clientes y devuelve los IDs de sus fichas en dólares.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
tax_number (str): CUIT o DNI del cliente objetivo.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
set[int]: IDs de las fichas en dólares; vacío si no se pudo buscar.
|
|
329
|
+
"""
|
|
330
|
+
keys = self._get_page_session_keys(SEARCH_URL)
|
|
331
|
+
if not keys:
|
|
332
|
+
return set()
|
|
333
|
+
|
|
334
|
+
payload = search_client_payload(keys[0], tax_number=get_national_id(tax_number))
|
|
335
|
+
return extract_usd_client_ids(parse_html(self._send_post(SEARCH_URL, payload, HEADERS_AJAX).text))
|
|
336
|
+
|
|
307
337
|
@require_login
|
|
308
338
|
def _search_item(self, sku: str) -> str | None:
|
|
309
339
|
"""Busca un producto por SKU en el webservice itemGetText.
|
|
@@ -11,7 +11,7 @@ from requests import RequestException, Response
|
|
|
11
11
|
from gbp.domain.models import Client
|
|
12
12
|
from gbp.http.headers import HEADERS_AJAX
|
|
13
13
|
from gbp.http.urls import BASE_URL, EDIT_URL, SEARCH_URL
|
|
14
|
-
from gbp.parsers import get_input_value, get_seller_id, get_state_id, parse_client_form, parse_html
|
|
14
|
+
from gbp.parsers import extract_client_edit_keys, get_input_value, get_seller_id, get_state_id, parse_client_form, parse_html
|
|
15
15
|
from gbp.payloads import clean_client_payload, create_client_payload, search_client_payload, update_client_payload
|
|
16
16
|
from gbp.decorator import require_login
|
|
17
17
|
from gbp.services.base import BaseGBPService
|
|
@@ -387,12 +387,9 @@ class CustomerService(BaseGBPService):
|
|
|
387
387
|
html (str): Contenido HTML a analizar.
|
|
388
388
|
|
|
389
389
|
Returns:
|
|
390
|
-
list[str]:
|
|
390
|
+
list[str]: Una URL por cliente, primero las fichas en dólares.
|
|
391
391
|
"""
|
|
392
|
-
|
|
393
|
-
matches = re.findall(pattern, html)
|
|
394
|
-
urls = [f"{BASE_URL}/wfmGenericEditNew.aspx{match}" for match in matches]
|
|
395
|
-
return list(dict.fromkeys(urls))
|
|
392
|
+
return [f"{BASE_URL}/wfmGenericEditNew.aspx{key}" for key in extract_client_edit_keys(parse_html(html))]
|
|
396
393
|
|
|
397
394
|
def _is_same_tax_number(self, candidate_tax_number: str, requested_tax_number: str) -> bool:
|
|
398
395
|
"""Valida coincidencia por CUIT completo o por DNI derivado del CUIT.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "clientGBP"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.2"
|
|
4
4
|
description = "Libreria para el uso de GBP mediante HTTP. Sin utilizar interfaz gráfica (Scraping).Usando las credenciales de una cuenta, se obtiene las cookies de sesión y se pueden realizar operaciones. Se realiza todo de manera sincrona."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Leandro Franco",email = "leandro00fr@gmail.com"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|