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.
Files changed (41) hide show
  1. {clientgbp-2.2.0 → clientgbp-2.2.2}/PKG-INFO +1 -1
  2. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/__init__.py +3 -0
  3. clientgbp-2.2.2/gbp/parsers/customers.py +35 -0
  4. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/ws.py +3 -3
  5. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/base.py +60 -30
  6. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/customers.py +3 -6
  7. {clientgbp-2.2.0 → clientgbp-2.2.2}/pyproject.toml +1 -1
  8. {clientgbp-2.2.0 → clientgbp-2.2.2}/LICENSE +0 -0
  9. {clientgbp-2.2.0 → clientgbp-2.2.2}/README.md +0 -0
  10. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/__init__.py +0 -0
  11. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/client.py +0 -0
  12. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/decorator/__init__.py +0 -0
  13. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/decorator/require_login.py +0 -0
  14. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/domain/__init__.py +0 -0
  15. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/domain/enums.py +0 -0
  16. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/domain/models.py +0 -0
  17. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/http/__init__.py +0 -0
  18. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/http/headers.py +0 -0
  19. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/http/http.py +0 -0
  20. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/http/urls.py +0 -0
  21. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/entities.py +0 -0
  22. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/forms.py +0 -0
  23. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/html.py +0 -0
  24. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/items.py +0 -0
  25. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/parsers/orders.py +0 -0
  26. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/__init__.py +0 -0
  27. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/_constants.py +0 -0
  28. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/_form_utils.py +0 -0
  29. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/_viewstate_utils.py +0 -0
  30. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/auth.py +0 -0
  31. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/contacts.py +0 -0
  32. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/customers.py +0 -0
  33. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/payloads/orders.py +0 -0
  34. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/__init__.py +0 -0
  35. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/auth.py +0 -0
  36. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/contacts.py +0 -0
  37. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/gbp_service.py +0 -0
  38. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/orders.py +0 -0
  39. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/services/reports.py +0 -0
  40. {clientgbp-2.2.0 → clientgbp-2.2.2}/gbp/validators/__init__.py +0 -0
  41. {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.0
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": national_id,
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 get_input_value, match_item_to_requested_sku, parse_html, extract_sku_tail
16
- from gbp.payloads import crm_payload, item_search_payload
17
- from gbp.validators import normalize_valid_tax_number
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
- pattern = r"wfmGenericEditNew\.aspx','(\?[^']+)'"
229
- matches = re.findall(pattern, html)
230
- if matches:
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
- try:
281
- resp_customer = self._send_post_json(CUSTOMER_WS_URL, crm_payload(normalized_tax_number), HEADERS_JSON_CONTACT)
282
- logger.debug(resp_customer.text)
283
- if resp_customer.status_code != 200:
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
- # Obtiene los resultados encontrados y lo parsea a JSON para manejarlo de mejor manera.
288
- customer_data = resp_customer.json()
289
- clients_crm = customer_data["d"]
290
- total_clients = len(clients_crm)
291
- logger.info(f"Clientes encontrados para CUIT {normalized_tax_number}: {clients_crm}")
292
- if total_clients == 0:
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 total_clients > 1:
297
- usd_crm_clients = [client for client in clients_crm if "PESOS" not in client]
298
- if usd_crm_clients:
299
- return usd_crm_clients[0]
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 customer_data["d"][0]
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]: Lista de URLs de edición sin duplicados.
390
+ list[str]: Una URL por cliente, primero las fichas en dólares.
391
391
  """
392
- pattern = r"wfmGenericEditNew\.aspx','(\?[^']+)'"
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.0"
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