producteca 2.0.33__py3-none-any.whl → 2.0.57__py3-none-any.whl
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.
- producteca/abstract/abstract_dataclass.py +4 -2
- producteca/payments/payments.py +3 -3
- producteca/products/products.py +73 -64
- producteca/products/search_products.py +19 -19
- producteca/products/tests/test_products.py +0 -10
- producteca/sales_orders/sales_orders.py +44 -16
- producteca/sales_orders/search_sale_orders.py +8 -8
- producteca/utils.py +50 -0
- {producteca-2.0.33.dist-info → producteca-2.0.57.dist-info}/METADATA +1 -1
- {producteca-2.0.33.dist-info → producteca-2.0.57.dist-info}/RECORD +13 -12
- {producteca-2.0.33.dist-info → producteca-2.0.57.dist-info}/LICENSE +0 -0
- {producteca-2.0.33.dist-info → producteca-2.0.57.dist-info}/WHEEL +0 -0
- {producteca-2.0.33.dist-info → producteca-2.0.57.dist-info}/entry_points.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from abc import ABC
|
|
2
2
|
from ..config.config import ConfigProducteca
|
|
3
|
+
from ..utils import clean_model_dump
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Optional, Any
|
|
5
6
|
|
|
@@ -14,10 +15,11 @@ class BaseService(ABC):
|
|
|
14
15
|
return repr(self._record)
|
|
15
16
|
|
|
16
17
|
def to_dict(self):
|
|
17
|
-
return self._record
|
|
18
|
+
return clean_model_dump(self._record)
|
|
18
19
|
|
|
19
20
|
def to_json(self):
|
|
20
|
-
|
|
21
|
+
import json
|
|
22
|
+
return json.dumps(clean_model_dump(self._record))
|
|
21
23
|
|
|
22
24
|
def __getattr__(self, key):
|
|
23
25
|
return getattr(self._record, key)
|
producteca/payments/payments.py
CHANGED
|
@@ -20,14 +20,14 @@ class Payment(BaseModel):
|
|
|
20
20
|
date: str
|
|
21
21
|
amount: float
|
|
22
22
|
couponAmount: Optional[float] = None
|
|
23
|
-
status: str
|
|
24
|
-
method: str
|
|
23
|
+
status: Optional[str] = None
|
|
24
|
+
method: Optional[str] = None
|
|
25
25
|
integration: Optional[PaymentIntegration] = None
|
|
26
26
|
transactionFee: Optional[float] = None
|
|
27
27
|
installments: Optional[int] = None
|
|
28
28
|
card: Optional[PaymentCard] = None
|
|
29
29
|
notes: Optional[str] = None
|
|
30
|
-
hasCancelableStatus: bool
|
|
30
|
+
hasCancelableStatus: Optional[bool] = None
|
|
31
31
|
id: Optional[int] = None
|
|
32
32
|
|
|
33
33
|
|
producteca/products/products.py
CHANGED
|
@@ -3,6 +3,7 @@ from pydantic import BaseModel, Field, ValidationError
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from producteca.abstract.abstract_dataclass import BaseService
|
|
5
5
|
from producteca.products.search_products import SearchProduct, SearchProductParams
|
|
6
|
+
from producteca.utils import clean_model_dump
|
|
6
7
|
import logging
|
|
7
8
|
import requests
|
|
8
9
|
|
|
@@ -69,13 +70,13 @@ class Integration(BaseModel):
|
|
|
69
70
|
class Variation(BaseModel):
|
|
70
71
|
variation_id: Optional[int] = Field(default=None, alias='variationId')
|
|
71
72
|
components: Optional[List] = None
|
|
72
|
-
pictures: Optional[List[Picture]] = None
|
|
73
|
-
stocks: Optional[List[Stock]] = None
|
|
73
|
+
pictures: Optional[Union[List[Picture], List]] = None
|
|
74
|
+
stocks: Optional[Union[List[Stock], List]] = None
|
|
74
75
|
attributes_hash: Optional[str] = Field(default=None, alias='attributesHash')
|
|
75
76
|
primary_color: Optional[str] = Field(default=None, alias='primaryColor')
|
|
76
77
|
thumbnail: Optional[str] = None
|
|
77
|
-
attributes: Optional[List[Attribute]] = None
|
|
78
|
-
integrations: Optional[List[Integration]] = None
|
|
78
|
+
attributes: Optional[Union[List[Attribute], List]] = None
|
|
79
|
+
integrations: Optional[Union[List[Integration], List]] = None
|
|
79
80
|
id: Optional[int] = None
|
|
80
81
|
sku: Optional[str] = None
|
|
81
82
|
barcode: Optional[str] = None
|
|
@@ -96,34 +97,35 @@ class BundleComponent(BaseModel):
|
|
|
96
97
|
|
|
97
98
|
class BundleVariation(BaseModel):
|
|
98
99
|
variation_id: int = Field(alias='variationId')
|
|
99
|
-
components: List[BundleComponent]
|
|
100
|
+
components: Union[List[BundleComponent], List]
|
|
100
101
|
|
|
101
102
|
|
|
102
103
|
class BundleResult(BaseModel):
|
|
103
104
|
company_id: int = Field(alias='companyId')
|
|
104
105
|
product_id: int = Field(alias='productId')
|
|
105
|
-
variations: List[BundleVariation]
|
|
106
|
+
variations: Union[List[BundleVariation], List]
|
|
106
107
|
id: str
|
|
107
108
|
|
|
108
109
|
|
|
109
110
|
class BundleResponse(BaseModel):
|
|
110
|
-
results: List[BundleResult]
|
|
111
|
+
results: Union[List[BundleResult], List]
|
|
111
112
|
count: int
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
class Product(BaseModel):
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
updatable_properties: Optional[List[str]] = Field(default=None, alias='$updatableProperties')
|
|
117
|
+
integrations: Optional[Union[List[Integration], List]] = None
|
|
118
|
+
variations: Optional[Union[List[Variation], List]] = None
|
|
117
119
|
is_simple: Optional[bool] = Field(default=None, alias='isSimple')
|
|
118
120
|
has_variations: Optional[bool] = Field(default=None, alias='hasVariations')
|
|
119
121
|
thumbnail: Optional[str] = None
|
|
120
122
|
category: Optional[str] = None
|
|
121
123
|
notes: Optional[str] = None
|
|
122
|
-
prices: Optional[List[Price]] = None
|
|
124
|
+
prices: Optional[Union[List[Price], List]] = None
|
|
123
125
|
buying_price: Optional[float] = Field(default=None, alias='buyingPrice')
|
|
124
126
|
is_archived: Optional[bool] = Field(default=None, alias='isArchived')
|
|
125
|
-
dimensions: Optional[Dimensions] = None
|
|
126
|
-
attributes: Optional[List[Attribute]] = None
|
|
127
|
+
dimensions: Optional[Union[Dimensions, dict]] = None
|
|
128
|
+
attributes: Optional[Union[List[Attribute], List]] = None
|
|
127
129
|
metadata: Optional[List[str]] = None
|
|
128
130
|
is_original: Optional[bool] = Field(default=None, alias='isOriginal')
|
|
129
131
|
name: str
|
|
@@ -138,21 +140,21 @@ class ProductVariationBase(BaseModel):
|
|
|
138
140
|
variation_id: Optional[int] = Field(default=None, alias='variationId')
|
|
139
141
|
code: Optional[str] = None
|
|
140
142
|
barcode: Optional[str] = None
|
|
141
|
-
attributes: List[Attribute] = []
|
|
143
|
+
attributes: Union[List[Attribute], List] = []
|
|
142
144
|
tags: Optional[List[str]] = []
|
|
143
|
-
buying_price: Optional[float] = Field(
|
|
144
|
-
dimensions: Optional[Dimensions] = Field(default_factory=Dimensions)
|
|
145
|
+
buying_price: Optional[float] = Field(default=None, alias='buyingPrice')
|
|
146
|
+
dimensions: Optional[Union[Dimensions, dict]] = Field(default_factory=Dimensions)
|
|
145
147
|
brand: Optional[str] = ''
|
|
146
148
|
notes: Optional[str] = ''
|
|
147
|
-
deals: Optional[List[Deal]] = []
|
|
148
|
-
stocks: List[Stock]
|
|
149
|
-
prices: Optional[List[Price]] = []
|
|
150
|
-
pictures: Optional[List[Picture]] = []
|
|
149
|
+
deals: Optional[Union[List[Deal], List]] = []
|
|
150
|
+
stocks: Optional[Union[List[Stock], List]] = []
|
|
151
|
+
prices: Optional[Union[List[Price], List]] = []
|
|
152
|
+
pictures: Optional[Union[List[Picture], List]] = []
|
|
151
153
|
|
|
152
154
|
|
|
153
155
|
class ProductVariation(ProductVariationBase):
|
|
154
156
|
category: Optional[str] = Field(default=None)
|
|
155
|
-
name: str
|
|
157
|
+
name: Optional[str] = None
|
|
156
158
|
|
|
157
159
|
|
|
158
160
|
class Shipping(BaseModel):
|
|
@@ -180,11 +182,11 @@ class MeliProduct(BaseModel):
|
|
|
180
182
|
product_id: Optional[int] = Field(default=None, alias='productId')
|
|
181
183
|
tags: Optional[List[str]] = Field(default=None)
|
|
182
184
|
has_custom_shipping_costs: Optional[bool] = Field(default=None, alias='hasCustomShippingCosts')
|
|
183
|
-
shipping: Optional[Shipping] = None
|
|
184
|
-
mshops_shipping: Optional[MShopsShipping] = Field(default=None, alias='mShopsShipping')
|
|
185
|
+
shipping: Optional[Union[Shipping, dict]] = None
|
|
186
|
+
mshops_shipping: Optional[Union[MShopsShipping, dict]] = Field(default=None, alias='mShopsShipping')
|
|
185
187
|
add_free_shipping_cost_to_price: Optional[bool] = Field(default=None, alias='addFreeShippingCostToPrice')
|
|
186
|
-
category: MeliCategory
|
|
187
|
-
attribute_completion: Optional[AttributeCompletion] = Field(default=None, alias='attributeCompletion')
|
|
188
|
+
category: Union[MeliCategory, dict]
|
|
189
|
+
attribute_completion: Optional[Union[AttributeCompletion, dict]] = Field(default=None, alias='attributeCompletion')
|
|
188
190
|
catalog_products: Optional[List[str]] = Field(default=None, alias='catalogProducts')
|
|
189
191
|
warranty: Optional[str] = None
|
|
190
192
|
domain: Optional[str] = None
|
|
@@ -211,8 +213,8 @@ class ResolvedValue(BaseModel):
|
|
|
211
213
|
|
|
212
214
|
class ResolvedError(BaseModel):
|
|
213
215
|
resolved: Optional[bool] = None
|
|
214
|
-
reason: Optional[ErrorReason] = None
|
|
215
|
-
value: Optional[ResolvedValue] = None
|
|
216
|
+
reason: Optional[Union[ErrorReason, dict]] = None
|
|
217
|
+
value: Optional[Union[ResolvedValue, dict]] = None
|
|
216
218
|
statusCode: Optional[int] = None
|
|
217
219
|
|
|
218
220
|
|
|
@@ -227,20 +229,20 @@ class ErrorContext(BaseModel):
|
|
|
227
229
|
|
|
228
230
|
|
|
229
231
|
class SynchronizeResponse(BaseModel):
|
|
230
|
-
product: Optional[ResolvedError] = None
|
|
231
|
-
variation: Optional[ResolvedError] = None
|
|
232
|
-
deals: Optional[ResolvedError] = None
|
|
233
|
-
bundles: Optional[ResolvedError] = None
|
|
234
|
-
taxes: Optional[ResolvedError] = None
|
|
235
|
-
meliProductListingIntegrations: Optional[ResolvedError] = None
|
|
236
|
-
tags: Optional[ResolvedError] = None
|
|
237
|
-
productIntegrations: Optional[ResolvedError] = None
|
|
232
|
+
product: Optional[Union[ResolvedError, dict]] = None
|
|
233
|
+
variation: Optional[Union[ResolvedError, dict]] = None
|
|
234
|
+
deals: Optional[Union[ResolvedError, dict]] = None
|
|
235
|
+
bundles: Optional[Union[ResolvedError, dict]] = None
|
|
236
|
+
taxes: Optional[Union[ResolvedError, dict]] = None
|
|
237
|
+
meliProductListingIntegrations: Optional[Union[ResolvedError, dict]] = None
|
|
238
|
+
tags: Optional[Union[ResolvedError, dict]] = None
|
|
239
|
+
productIntegrations: Optional[Union[ResolvedError, dict]] = None
|
|
238
240
|
statusCode: Optional[int] = None
|
|
239
|
-
error_context: Optional[ErrorContext] = Field(None, alias='error@context')
|
|
241
|
+
error_context: Optional[Union[ErrorContext, dict]] = Field(None, alias='error@context')
|
|
240
242
|
|
|
241
243
|
|
|
242
244
|
class ListedSynchronizeResponse(BaseModel):
|
|
243
|
-
results: List[SynchronizeResponse]
|
|
245
|
+
results: Union[List[SynchronizeResponse], List]
|
|
244
246
|
|
|
245
247
|
|
|
246
248
|
@dataclass
|
|
@@ -253,49 +255,51 @@ class ProductService(BaseService):
|
|
|
253
255
|
return self
|
|
254
256
|
|
|
255
257
|
def synchronize(self, payload) -> Union[Product, SynchronizeResponse]:
|
|
256
|
-
|
|
257
258
|
endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
|
|
258
259
|
headers = self.config.headers.copy()
|
|
259
260
|
headers.update({"createifitdoesntexist": str(self.create_if_it_doesnt_exist).lower()})
|
|
260
261
|
product_variation = ProductVariation(**payload)
|
|
261
262
|
if not product_variation.code and not product_variation.sku:
|
|
262
263
|
raise Exception("Sku or code should be provided to update the product")
|
|
263
|
-
|
|
264
|
+
# Hacer model_dump con limpieza automática de valores vacíos
|
|
265
|
+
data = clean_model_dump(product_variation)
|
|
266
|
+
_logger.info(f"Synchronizing product: {data}")
|
|
267
|
+
_logger.info(f"POST {endpoint_url} - Headers: {headers} - Data: {data}")
|
|
264
268
|
response = requests.post(endpoint_url, json=data, headers=headers)
|
|
269
|
+
if not response.ok:
|
|
270
|
+
raise Exception(f"Error getting product {product_variation.sku} - {product_variation.code}\n {response.text}")
|
|
271
|
+
if response.status_code == 204:
|
|
272
|
+
_logger.info("Status code is 204 (No Content), product synchronized successfully but no changes were made")
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
_logger.info(f"response text: {response.text}")
|
|
265
276
|
response_data = response.json()
|
|
266
|
-
|
|
267
|
-
return Product(**response_data)
|
|
268
|
-
except ValidationError:
|
|
269
|
-
pass
|
|
277
|
+
_logger.debug(f"Response data: {response_data}")
|
|
270
278
|
if isinstance(response_data, list):
|
|
271
279
|
res = ListedSynchronizeResponse(results=response_data)
|
|
272
|
-
if
|
|
280
|
+
if res.results and hasattr(res.results[0], 'error_context') and res.results[0].error_context:
|
|
273
281
|
raise Exception(f"Errored while updating {res.results[0].error_context} {res.model_dump_json()}")
|
|
274
|
-
else
|
|
275
|
-
return res.results[0]
|
|
276
|
-
else:
|
|
277
|
-
try:
|
|
278
|
-
sync_resp = SynchronizeResponse(**response_data)
|
|
279
|
-
if sync_resp.error_context:
|
|
280
|
-
raise Exception(f"Errored while updating {sync_resp.error_context} - {sync_resp.model_dump_json()}")
|
|
281
|
-
else:
|
|
282
|
-
return sync_resp
|
|
283
|
-
except ValidationError:
|
|
284
|
-
try:
|
|
285
|
-
error_res = ErrorReason(**response_data)
|
|
286
|
-
raise Exception(f"Errored with the following message {error_res.message} - {error_res.model_dump_json()}")
|
|
287
|
-
except ValidationError:
|
|
288
|
-
pass
|
|
282
|
+
return res.results[0] if res.results else None
|
|
289
283
|
|
|
290
|
-
if
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
284
|
+
if isinstance(response_data, dict) and 'name' in response_data:
|
|
285
|
+
return Product(**response_data)
|
|
286
|
+
|
|
287
|
+
if isinstance(response_data, dict) and any(key in response_data for key in ['product', 'variation', 'statusCode']):
|
|
288
|
+
sync_resp = SynchronizeResponse(**response_data)
|
|
289
|
+
if sync_resp.error_context:
|
|
290
|
+
raise Exception(f"Errored while updating {sync_resp.error_context} - {sync_resp.model_dump_json()}")
|
|
291
|
+
return sync_resp
|
|
292
|
+
|
|
293
|
+
if isinstance(response_data, dict) and 'message' in response_data:
|
|
294
|
+
error_res = ErrorReason(**response_data)
|
|
295
|
+
raise Exception(f"Errored with the following message {error_res.message} - {error_res.model_dump_json()}")
|
|
296
|
+
|
|
297
|
+
raise Exception(f"Unhandled response format, check response {response.text}")
|
|
295
298
|
|
|
296
299
|
def get(self, product_id: int) -> "ProductService":
|
|
297
300
|
endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}')
|
|
298
301
|
headers = self.config.headers
|
|
302
|
+
_logger.info(f"GET {endpoint_url} - Headers: {headers}")
|
|
299
303
|
response = requests.get(endpoint_url, headers=headers)
|
|
300
304
|
if not response.ok:
|
|
301
305
|
raise Exception(f"Error getting product {product_id}\n {response.text}")
|
|
@@ -305,6 +309,7 @@ class ProductService(BaseService):
|
|
|
305
309
|
def get_bundle(self, product_id: int) -> BundleResponse:
|
|
306
310
|
endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}/bundles')
|
|
307
311
|
headers = self.config.headers
|
|
312
|
+
_logger.info(f"GET {endpoint_url} - Headers: {headers}")
|
|
308
313
|
response = requests.get(endpoint_url, headers=headers)
|
|
309
314
|
if not response.ok:
|
|
310
315
|
raise Exception(f"Error getting bundle {product_id}\n {response.text}")
|
|
@@ -313,6 +318,7 @@ class ProductService(BaseService):
|
|
|
313
318
|
def get_ml_integration(self, product_id: int) -> MeliProduct:
|
|
314
319
|
endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}/listingintegration')
|
|
315
320
|
headers = self.config.headers
|
|
321
|
+
_logger.info(f"GET {endpoint_url} - Headers: {headers}")
|
|
316
322
|
response = requests.get(endpoint_url, headers=headers)
|
|
317
323
|
if not response.ok:
|
|
318
324
|
raise Exception(f"Error getting ml integration {product_id}\n {response.text}")
|
|
@@ -323,7 +329,10 @@ class ProductService(BaseService):
|
|
|
323
329
|
endpoint: str = f'search/{self.endpoint}'
|
|
324
330
|
headers = self.config.headers
|
|
325
331
|
url = self.config.get_endpoint(endpoint)
|
|
326
|
-
|
|
332
|
+
params_dict = clean_model_dump(params)
|
|
333
|
+
_logger.info(f"GET {url} - Headers: {headers} - Params: {params_dict}")
|
|
334
|
+
response = requests.get(url, headers=headers, params=params_dict)
|
|
335
|
+
_logger.info(f"Response status: {response.status_code} - Response text: {response.text}")
|
|
327
336
|
if not response.ok:
|
|
328
337
|
raise Exception(f"error in searching products {response.text}")
|
|
329
338
|
return SearchProduct(**response.json())
|
|
@@ -56,24 +56,24 @@ class SearchDeals(BaseModel):
|
|
|
56
56
|
class SearchResultItem(BaseModel):
|
|
57
57
|
search_score: float = Field(..., alias='@search.score')
|
|
58
58
|
id: int
|
|
59
|
-
product_id: int = Field(
|
|
60
|
-
company_id: int = Field(
|
|
61
|
-
name:
|
|
62
|
-
code: str
|
|
59
|
+
product_id: Optional[int] = Field(None, alias='productId')
|
|
60
|
+
company_id: Optional[int] = Field(None, alias='companyId')
|
|
61
|
+
name:Optional[str] = None
|
|
62
|
+
code: Optional[str] = None
|
|
63
63
|
skus: List[str]
|
|
64
|
-
brand: str
|
|
65
|
-
category: str
|
|
66
|
-
thumbnail: str
|
|
67
|
-
stocks: List[SearchStocks]
|
|
68
|
-
warehouses_with_stock: List[str] = Field(
|
|
69
|
-
total_stock: int = Field(
|
|
70
|
-
has_pictures: bool = Field(
|
|
71
|
-
buying_price: float = Field(
|
|
72
|
-
prices: List[SearchPrices]
|
|
73
|
-
integration_ids: List[str] = Field(
|
|
74
|
-
integration_apps: List[str] = Field(
|
|
75
|
-
integrations: List[SearchIntegration]
|
|
76
|
-
campaigns: List[str]
|
|
64
|
+
brand: Optional[str] = None
|
|
65
|
+
category: Optional[str] = None
|
|
66
|
+
thumbnail: Optional[str] = None
|
|
67
|
+
stocks: Optional[Union[List[SearchStocks], List]] = None
|
|
68
|
+
warehouses_with_stock: Optional[List[str]] = Field(None, alias='warehousesWithStock')
|
|
69
|
+
total_stock: Optional[int] = Field(None, alias='totalStock')
|
|
70
|
+
has_pictures: Optional[bool] = Field(None, alias='hasPictures')
|
|
71
|
+
buying_price: Optional[float] = Field(None, alias='buyingPrice')
|
|
72
|
+
prices: Optional[Union[List[SearchPrices], List]] = None
|
|
73
|
+
integration_ids: Optional[List[str]] = Field(None, alias='integrationIds')
|
|
74
|
+
integration_apps: Optional[List[str]] = Field(None, alias='integrationApps')
|
|
75
|
+
integrations: Optional[Union[List[SearchIntegration], List]] = None
|
|
76
|
+
campaigns: Optional[List[str]] = None
|
|
77
77
|
app: Optional[int] = None
|
|
78
78
|
status: Optional[str] = None
|
|
79
79
|
synchronize_stock: Optional[bool] = Field(None, alias='synchronizeStock')
|
|
@@ -96,7 +96,7 @@ class SearchResultItem(BaseModel):
|
|
|
96
96
|
mandatory_free_shipping: Optional[bool] = Field(None, alias='mandatoryFreeShipping')
|
|
97
97
|
free_shipping: Optional[bool] = Field(None, alias='freeShipping')
|
|
98
98
|
free_shipping_cost: Optional[float] = Field(None, alias='freeShippingCost')
|
|
99
|
-
template: Optional[
|
|
99
|
+
template: Optional[int] = None
|
|
100
100
|
youtube_id: Optional[str] = Field(None, alias='youtubeId')
|
|
101
101
|
warranty: Optional[str] = None
|
|
102
102
|
permalink: Optional[str] = None
|
|
@@ -104,7 +104,7 @@ class SearchResultItem(BaseModel):
|
|
|
104
104
|
attribute_completion_status: Optional[str] = Field(None, alias='attributeCompletionStatus')
|
|
105
105
|
attribute_completion_count: Optional[int] = Field(None, alias='attributeCompletionCount')
|
|
106
106
|
attribute_completion_total: Optional[int] = Field(None, alias='attributeCompletionTotal')
|
|
107
|
-
deals: Optional[SearchDeals] = None
|
|
107
|
+
deals: Optional[Union[SearchDeals, List]] = None
|
|
108
108
|
campaign_status: Optional[List[str]] = Field(None, alias='campaignStatus')
|
|
109
109
|
size_chart: Optional[str] = Field(None, alias='sizeChart')
|
|
110
110
|
channel_status: Optional[List[str]] = Field(None, alias='channelStatus')
|
|
@@ -53,16 +53,6 @@ class TestProduct(unittest.TestCase):
|
|
|
53
53
|
|
|
54
54
|
self.assertEqual(response.sku, "TEST001")
|
|
55
55
|
|
|
56
|
-
@patch('requests.post')
|
|
57
|
-
def test_create_product_not_exist(self, mock_post):
|
|
58
|
-
# Mock product not found response
|
|
59
|
-
mock_response = Mock()
|
|
60
|
-
mock_response.status_code = 204
|
|
61
|
-
mock_post.return_value = mock_response
|
|
62
|
-
|
|
63
|
-
with self.assertRaises(Exception):
|
|
64
|
-
self.client.Product.synchronize(self.product_to_create_payload)
|
|
65
|
-
|
|
66
56
|
@patch('requests.post')
|
|
67
57
|
def test_update_product_success(self, mock_post):
|
|
68
58
|
payload = self.product_to_create_payload
|
|
@@ -5,6 +5,7 @@ from producteca.abstract.abstract_dataclass import BaseService
|
|
|
5
5
|
from producteca.sales_orders.search_sale_orders import SearchSalesOrderParams, SearchSalesOrder
|
|
6
6
|
from producteca.payments.payments import Payment
|
|
7
7
|
from producteca.shipments.shipment import Shipment
|
|
8
|
+
from producteca.utils import clean_model_dump
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
import logging
|
|
10
11
|
_logger = logging.getLogger(__name__)
|
|
@@ -193,10 +194,10 @@ class SaleOrderShipment(BaseModel):
|
|
|
193
194
|
|
|
194
195
|
|
|
195
196
|
class SaleOrderInvoiceIntegrationAbstract(BaseModel):
|
|
196
|
-
id: int
|
|
197
|
-
integration_id: str = Field(alias="integrationId")
|
|
198
|
-
app: int
|
|
199
|
-
created_at: str = Field(alias="createdAt")
|
|
197
|
+
id: Optional[int] = None
|
|
198
|
+
integration_id: Optional[str] = Field(None, alias="integrationId")
|
|
199
|
+
app: Optional[int] = None
|
|
200
|
+
created_at: Optional[str] = Field(None, alias="createdAt")
|
|
200
201
|
decrease_stock: Optional[bool] = Field(None, alias="decreaseStock")
|
|
201
202
|
|
|
202
203
|
|
|
@@ -287,9 +288,10 @@ class SaleOrderService(BaseService):
|
|
|
287
288
|
def get(self, sale_order_id: int) -> "SaleOrderService":
|
|
288
289
|
endpoint = f'{self.endpoint}/{sale_order_id}'
|
|
289
290
|
url = self.config.get_endpoint(endpoint)
|
|
291
|
+
_logger.info(f"GET {url} - Headers: {self.config.headers}")
|
|
290
292
|
response = requests.get(url, headers=self.config.headers)
|
|
291
293
|
if not response.ok:
|
|
292
|
-
raise Exception(f"Order {sale_order_id} could not be fetched")
|
|
294
|
+
raise Exception(f"Order {sale_order_id} could not be fetched. Error {response.status_code} {response.text}")
|
|
293
295
|
response_data = response.json()
|
|
294
296
|
return self(**response_data)
|
|
295
297
|
|
|
@@ -298,6 +300,7 @@ class SaleOrderService(BaseService):
|
|
|
298
300
|
raise Exception("You need to add a record id")
|
|
299
301
|
endpoint = f'{self.endpoint}/{self._record.id}/labels'
|
|
300
302
|
url = self.config.get_endpoint(endpoint)
|
|
303
|
+
_logger.info(f"GET {url} - Headers: {self.config.headers}")
|
|
301
304
|
response = requests.get(url, headers=self.config.headers)
|
|
302
305
|
if not response.ok:
|
|
303
306
|
raise Exception("labels could not be gotten")
|
|
@@ -308,6 +311,7 @@ class SaleOrderService(BaseService):
|
|
|
308
311
|
raise Exception("You need to add a record id")
|
|
309
312
|
endpoint = f'{self.endpoint}/{self._record.id}/close'
|
|
310
313
|
url = self.config.get_endpoint(endpoint)
|
|
314
|
+
_logger.info(f"POST {url} - Headers: {self.config.headers}")
|
|
311
315
|
response = requests.post(url, headers=self.config.headers)
|
|
312
316
|
if not response.ok:
|
|
313
317
|
raise Exception("Order could not be closed")
|
|
@@ -317,6 +321,7 @@ class SaleOrderService(BaseService):
|
|
|
317
321
|
raise Exception("You need to add a record id")
|
|
318
322
|
endpoint = f'{self.endpoint}/{self._record.id}/cancel'
|
|
319
323
|
url = self.config.get_endpoint(endpoint)
|
|
324
|
+
_logger.info(f"POST {url} - Headers: {self.config.headers}")
|
|
320
325
|
response = requests.post(url, headers=self.config.headers)
|
|
321
326
|
if not response.ok:
|
|
322
327
|
raise Exception("Order could not be cancelled")
|
|
@@ -327,8 +332,10 @@ class SaleOrderService(BaseService):
|
|
|
327
332
|
endpoint = f'{self.endpoint}/synchronize'
|
|
328
333
|
url = self.config.get_endpoint(endpoint)
|
|
329
334
|
# TODO: Check what can we sync, and what can we not sync
|
|
330
|
-
sync_body = SaleOrderSynchronize(**self._record
|
|
331
|
-
|
|
335
|
+
sync_body = SaleOrderSynchronize(**clean_model_dump(self._record))
|
|
336
|
+
sync_data = clean_model_dump(sync_body)
|
|
337
|
+
_logger.info(f"POST {url} - Headers: {self.config.headers} - Data: {sync_data}")
|
|
338
|
+
response = requests.post(url, json=sync_data, headers=self.config.headers)
|
|
332
339
|
if not response.ok:
|
|
333
340
|
raise Exception(f"Synchronize error {response.status_code} {response.text}")
|
|
334
341
|
sync_res = SaleOrderSyncResponse(**response.json()) # noqa
|
|
@@ -337,11 +344,23 @@ class SaleOrderService(BaseService):
|
|
|
337
344
|
def invoice_integration(self):
|
|
338
345
|
if not self._record:
|
|
339
346
|
raise Exception("You need to add a record id")
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
347
|
+
|
|
348
|
+
invoice_integration_data = clean_model_dump(self._record.invoice_integration)
|
|
349
|
+
|
|
350
|
+
if self._record.invoice_integration.id:
|
|
351
|
+
endpoint = f'{self.endpoint}/{self._record.id}/invoiceIntegration'
|
|
352
|
+
url = self.config.get_endpoint(endpoint)
|
|
353
|
+
_logger.info(f"PUT {url} - Headers: {self.config.headers} - Data: {invoice_integration_data}")
|
|
354
|
+
response = requests.put(url, headers=self.config.headers,
|
|
355
|
+
json=invoice_integration_data)
|
|
356
|
+
else:
|
|
357
|
+
endpoint = f'{self.endpoint}/synchronize'
|
|
358
|
+
url = self.config.get_endpoint(endpoint)
|
|
359
|
+
sync_data = {"id": self._record.id, "invoiceIntegration": invoice_integration_data}
|
|
360
|
+
_logger.info(f"POST {url} - Headers: {self.config.headers} - Data: {sync_data}")
|
|
361
|
+
response = requests.post(url, headers=self.config.headers,
|
|
362
|
+
json=sync_data)
|
|
363
|
+
|
|
345
364
|
if not response.ok:
|
|
346
365
|
raise Exception(f"Error on resposne {response.text}")
|
|
347
366
|
return response.ok
|
|
@@ -351,6 +370,7 @@ class SaleOrderService(BaseService):
|
|
|
351
370
|
headers = self.config.headers
|
|
352
371
|
url = self.config.get_endpoint(endpoint)
|
|
353
372
|
new_url = f"{url}?$filter={params.filter}&top={params.top}&skip={params.skip}"
|
|
373
|
+
_logger.info(f"GET {new_url} - Headers: {headers}")
|
|
354
374
|
response = requests.get(
|
|
355
375
|
new_url,
|
|
356
376
|
headers=headers,
|
|
@@ -365,7 +385,9 @@ class SaleOrderService(BaseService):
|
|
|
365
385
|
raise Exception("You need to add a record id")
|
|
366
386
|
payment = Payment(**payload)
|
|
367
387
|
url = self.config.get_endpoint(f"{self.endpoint}/{self._record.id}/payments")
|
|
368
|
-
|
|
388
|
+
payment_data = clean_model_dump(payment)
|
|
389
|
+
_logger.info(f"POST {url} - Headers: {self.config.headers} - Data: {payment_data}")
|
|
390
|
+
res = requests.post(url, json=payment_data, headers=self.config.headers)
|
|
369
391
|
if not res.ok:
|
|
370
392
|
raise Exception(f"Error on resposne {res.text}")
|
|
371
393
|
return Payment(**res.json())
|
|
@@ -375,7 +397,9 @@ class SaleOrderService(BaseService):
|
|
|
375
397
|
raise Exception("You need to add a record id")
|
|
376
398
|
payment = Payment(**payload)
|
|
377
399
|
url = self.config.get_endpoint(f"{self.endpoint}/{self._record.id}/payments/{payment_id}")
|
|
378
|
-
|
|
400
|
+
payment_data = clean_model_dump(payment)
|
|
401
|
+
_logger.info(f"PUT {url} - Headers: {self.config.headers} - Data: {payment_data}")
|
|
402
|
+
res = requests.put(url, json=payment_data, headers=self.config.headers)
|
|
379
403
|
if not res.ok:
|
|
380
404
|
raise Exception(f"Error on payment update {res.text}")
|
|
381
405
|
return Payment(**res.json())
|
|
@@ -385,7 +409,9 @@ class SaleOrderService(BaseService):
|
|
|
385
409
|
raise Exception("You need to add a record id")
|
|
386
410
|
shipment = Shipment(**payload)
|
|
387
411
|
url = self.config.get_endpoint(f"{self.endpoint}/{self._record.id}/shipments")
|
|
388
|
-
|
|
412
|
+
shipment_data = clean_model_dump(shipment)
|
|
413
|
+
_logger.info(f"POST {url} - Headers: {self.config.headers} - Data: {shipment_data}")
|
|
414
|
+
res = requests.post(url, json=shipment_data, headers=self.config.headers)
|
|
389
415
|
if not res.ok:
|
|
390
416
|
raise Exception(f"Error on shipment add {res.text}")
|
|
391
417
|
return Shipment(**res.json())
|
|
@@ -395,7 +421,9 @@ class SaleOrderService(BaseService):
|
|
|
395
421
|
raise Exception("You need to add a record id")
|
|
396
422
|
shipment = Shipment(**payload)
|
|
397
423
|
url = self.config.get_endpoint(f"{self.endpoint}/{self._record.id}/shipments/{shipment_id}")
|
|
398
|
-
|
|
424
|
+
shipment_data = clean_model_dump(shipment)
|
|
425
|
+
_logger.info(f"PUT {url} - Headers: {self.config.headers} - Data: {shipment_data}")
|
|
426
|
+
res = requests.put(url, json=shipment_data, headers=self.config.headers)
|
|
399
427
|
if not res.ok:
|
|
400
428
|
raise Exception(f"Error on shipment update {res.text}")
|
|
401
429
|
return Shipment(**res.json())
|
|
@@ -8,7 +8,7 @@ _logger = logging.getLogger(__name__)
|
|
|
8
8
|
class SalesOrderProduct(BaseModel):
|
|
9
9
|
id: int
|
|
10
10
|
name: str
|
|
11
|
-
code: str
|
|
11
|
+
code: Optional[str] = None
|
|
12
12
|
brand: Optional[str] = None
|
|
13
13
|
|
|
14
14
|
|
|
@@ -20,8 +20,8 @@ class SalesOrderVariationAttribute(BaseModel):
|
|
|
20
20
|
class SalesOrderVariation(BaseModel):
|
|
21
21
|
id: int
|
|
22
22
|
attributes: Optional[List[SalesOrderVariationAttribute]] = None
|
|
23
|
-
sku: str
|
|
24
|
-
thumbnail: str
|
|
23
|
+
sku: Optional[str] = None
|
|
24
|
+
thumbnail: Optional[str] = None
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class SalesOrderLine(BaseModel):
|
|
@@ -48,15 +48,15 @@ class SalesOrderPaymentIntegration(BaseModel):
|
|
|
48
48
|
class SalesOrderPayment(BaseModel):
|
|
49
49
|
date: str
|
|
50
50
|
amount: float
|
|
51
|
-
coupon_amount: float = Field(alias="couponAmount")
|
|
52
|
-
status: str
|
|
51
|
+
coupon_amount: Optional[float] = Field(default=None, alias="couponAmount")
|
|
52
|
+
status: Optional[str] = None
|
|
53
53
|
method: str
|
|
54
54
|
integration: Optional[SalesOrderPaymentIntegration] = None
|
|
55
|
-
transaction_fee: float = Field(alias="transactionFee")
|
|
56
|
-
installments: int
|
|
55
|
+
transaction_fee: Optional[float] = Field(default=None, alias="transactionFee")
|
|
56
|
+
installments: Optional[int] = None
|
|
57
57
|
card: Optional[SalesOrderCard] = None
|
|
58
58
|
notes: Optional[str] = None
|
|
59
|
-
has_cancelable_status: bool = Field(alias="hasCancelableStatus")
|
|
59
|
+
has_cancelable_status: Optional[bool] = Field(default=None, alias="hasCancelableStatus")
|
|
60
60
|
id: int
|
|
61
61
|
|
|
62
62
|
|
producteca/utils.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for the producteca package
|
|
3
|
+
"""
|
|
4
|
+
from typing import Any, Dict, List, Union
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def exclude_empty_values(obj: Any) -> Any:
|
|
9
|
+
"""Recursively remove None, empty lists, empty strings, and empty dicts
|
|
10
|
+
|
|
11
|
+
Note: Preserves 0 values for numeric fields as they are valid prices/quantities
|
|
12
|
+
Special case: Preserves empty list [] for 'updatableProperties' to allow explicit updates
|
|
13
|
+
"""
|
|
14
|
+
if isinstance(obj, dict):
|
|
15
|
+
filtered_dict = {}
|
|
16
|
+
for k, v in obj.items():
|
|
17
|
+
# Special case: preserve empty list for updatableProperties
|
|
18
|
+
# This allows explicitly sending [] to clear/update the field
|
|
19
|
+
if k == 'updatableProperties' and v == []:
|
|
20
|
+
filtered_dict[k] = v
|
|
21
|
+
continue
|
|
22
|
+
|
|
23
|
+
# Skip None, empty lists, empty strings, empty dicts
|
|
24
|
+
if v is None or v == [] or v == "" or v == {}:
|
|
25
|
+
continue
|
|
26
|
+
# Keep numeric 0 values - they are valid for prices, quantities, etc.
|
|
27
|
+
filtered_dict[k] = exclude_empty_values(v)
|
|
28
|
+
return filtered_dict
|
|
29
|
+
elif isinstance(obj, list):
|
|
30
|
+
filtered_list = [exclude_empty_values(item) for item in obj if item is not None]
|
|
31
|
+
return [item for item in filtered_list if item != [] and item != "" and item != {}]
|
|
32
|
+
else:
|
|
33
|
+
return obj
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def clean_model_dump(model: BaseModel, by_alias: bool = True, exclude_none: bool = True, **kwargs) -> Dict[str, Any]:
|
|
37
|
+
"""
|
|
38
|
+
Enhanced model_dump that automatically cleans empty values
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
model: Pydantic model instance
|
|
42
|
+
by_alias: Use field aliases in output
|
|
43
|
+
exclude_none: Exclude None values
|
|
44
|
+
**kwargs: Additional arguments to pass to model_dump
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Clean dictionary with empty values removed
|
|
48
|
+
"""
|
|
49
|
+
raw_data = model.model_dump(by_alias=by_alias, exclude_none=exclude_none, **kwargs)
|
|
50
|
+
return exclude_empty_values(raw_data)
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
producteca/__init__.py,sha256=ML-Y_hCd8HOgNOVGiqGpbLqWAtvPXqQQaBQmj-A5Lvc,52
|
|
2
2
|
producteca/abstract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
producteca/abstract/abstract_dataclass.py,sha256=
|
|
3
|
+
producteca/abstract/abstract_dataclass.py,sha256=Axsf4ewa7Qua5aosvEXksR-fXoGF01_EUlOl1oKdkIk,597
|
|
4
4
|
producteca/client.py,sha256=4T4vLh_Zz56mG2S0P9Il1NEJ4Rj0g2mgXYA7KJjY72U,789
|
|
5
5
|
producteca/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
producteca/config/config.py,sha256=uTRaHLI9L7P_LPuFmuYgV50Aedz9MfDbXOZVZPFkOuI,658
|
|
7
7
|
producteca/payments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
producteca/payments/payments.py,sha256=
|
|
8
|
+
producteca/payments/payments.py,sha256=zhbv6zWb1i04wGdqiyQJ8Dqp435xNHEOsjnps-pAT58,893
|
|
9
9
|
producteca/payments/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
producteca/payments/tests/test_payments.py,sha256=wutk5zK2xRChTvKyTmi54Y_92PzNtUKZF6xWF298sFY,3032
|
|
11
11
|
producteca/products/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
producteca/products/products.py,sha256=
|
|
13
|
-
producteca/products/search_products.py,sha256=
|
|
12
|
+
producteca/products/products.py,sha256=G5ApUxeipHKRCQZnGdCYLxNE-m--6dOsYiQB_sMI_Dw,13835
|
|
13
|
+
producteca/products/search_products.py,sha256=wPrnChvzPKzVermtKbi-V7wBmILJhJToyoNtpOOeXc4,6172
|
|
14
14
|
producteca/products/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
producteca/products/tests/test_products.py,sha256=
|
|
15
|
+
producteca/products/tests/test_products.py,sha256=JMsWq9DDJZn8If1dz42WSJ93-akOm7flFQPm6WYeJus,5377
|
|
16
16
|
producteca/products/tests/test_search_products.py,sha256=ISkDClYaYnwo23a7_O4OlgPfc4dAZCMinLAi07yecEI,4941
|
|
17
17
|
producteca/sales_orders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
producteca/sales_orders/sales_orders.py,sha256=
|
|
19
|
-
producteca/sales_orders/search_sale_orders.py,sha256=
|
|
18
|
+
producteca/sales_orders/sales_orders.py,sha256=js7OoCcSn6P-eHa9IwJDym7kYa5EUteTwI8563VVxEo,18027
|
|
19
|
+
producteca/sales_orders/search_sale_orders.py,sha256=rvbPqVqYjRpM45QXUZp_6uCk1j7CRyfN5hKsinvj6Kc,5179
|
|
20
20
|
producteca/sales_orders/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
producteca/sales_orders/tests/search.json,sha256=v6LkTvQyeBdqxRc3iWwXH5Ugs7EYJAEOiuPuuj_rHYk,3508
|
|
22
22
|
producteca/sales_orders/tests/test_sales_orders.py,sha256=hPtUqp4GUpp9iBr3AMGsS7nxHQ5JHJy-DLuTxf9Eufs,4245
|
|
@@ -25,8 +25,9 @@ producteca/shipments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
25
25
|
producteca/shipments/shipment.py,sha256=YokJRBX1ZbUtZVc4AOAB188rbHFO0_MH5hFv5U9Qpjw,844
|
|
26
26
|
producteca/shipments/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
producteca/shipments/tests/test_shipment.py,sha256=0edX3WM-t4nbp53r_Lvhtk_Suc2QCnIUQ27I90qSUfE,2512
|
|
28
|
-
producteca
|
|
29
|
-
producteca-2.0.
|
|
30
|
-
producteca-2.0.
|
|
31
|
-
producteca-2.0.
|
|
32
|
-
producteca-2.0.
|
|
28
|
+
producteca/utils.py,sha256=krCUtr5cwu1787v24wSagSltPjY1ERe3E1whhT-SZKU,1961
|
|
29
|
+
producteca-2.0.57.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
30
|
+
producteca-2.0.57.dist-info/METADATA,sha256=hfixRYOulnUNZD58c7LEytb-rke71V8z1bSoQP6m5hQ,3809
|
|
31
|
+
producteca-2.0.57.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
32
|
+
producteca-2.0.57.dist-info/entry_points.txt,sha256=BFSDFLbB70p8YVZPiU7HDJdj2VyyLdMVa4ekLQAUAVc,125
|
|
33
|
+
producteca-2.0.57.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|