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.
@@ -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.model_dump(by_alias=True)
18
+ return clean_model_dump(self._record)
18
19
 
19
20
  def to_json(self):
20
- return self._record.model_dump_json(by_alias=True)
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)
@@ -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
 
@@ -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
- integrations: Optional[List[Integration]] = None
116
- variations: Optional[List[Variation]] = None
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(0, alias='buyingPrice')
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
- data = product_variation.model_dump(by_alias=True, exclude_none=True)
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
- try:
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 any([r.error_context for r in res.results]):
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 not response.ok:
291
- raise Exception(f"Error getting product {product_variation.sku} - {product_variation.code}\n {response.text}")
292
- if response.status_code == 204:
293
- raise Exception("Status code is 204, meaning nothing was updated or created")
294
- raise Exception(f"Unhandled error, check response {response.text}")
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
- response = requests.get(url, headers=headers, params=params.model_dump(by_alias=True, exclude_none=True))
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(..., alias='productId')
60
- company_id: int = Field(..., alias='companyId')
61
- name: str
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(..., alias='warehousesWithStock')
69
- total_stock: int = Field(..., alias='totalStock')
70
- has_pictures: bool = Field(..., alias='hasPictures')
71
- buying_price: float = Field(..., alias='buyingPrice')
72
- prices: List[SearchPrices]
73
- integration_ids: List[str] = Field(..., alias='integrationIds')
74
- integration_apps: List[str] = Field(..., alias='integrationApps')
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[str] = None
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.model_dump(by_alias=True))
331
- response = requests.post(url, json=sync_body.model_dump(by_alias=True), headers=self.config.headers)
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
- endpoint = f'{self.endpoint}/{self._record.id}/invoiceIntegration'
341
- url = self.config.get_endpoint(endpoint)
342
- response = requests.put(url, headers=self.config.headers,
343
- json={"id": self._record.id,
344
- "invoiceIntegration": SaleOrderInvoiceIntegrationPut(**self._record.invoice_integration.model_dump(by_alias=True)).model_dump()})
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
- res = requests.post(url, json=payment.model_dump(by_alias=True, exclude_none=True), headers=self.config.headers)
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
- res = requests.put(url, json=payment.model_dump(by_alias=True, exclude_none=True), headers=self.config.headers)
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
- res = requests.post(url, json=shipment.model_dump(by_alias=True, exclude_none=True), headers=self.config.headers)
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
- res = requests.put(url, json=shipment.model_dump(by_alias=True, exclude_none=True), headers=self.config.headers)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: producteca
3
- Version: 2.0.33
3
+ Version: 2.0.57
4
4
  Summary:
5
5
  Author: Chroma Agency, Matias Rivera
6
6
  Author-email: mrivera@chroma.agency
@@ -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=LpAsIdVgCbqT602_1X19mfnuooa5j6LttjvZKKJhw-Q,549
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=ac0X-QC4CGvxs40B_qWv0IepBVpj_nULNSNp8s3v5aA,842
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=FQeif3q52JGUcYVu5VKzY81FPSInQlc-HfYpYFlWmxo,12403
13
- producteca/products/search_products.py,sha256=qoo7ttdBuDUKEIOz4ysuBwK88aJ6PfSOtLkgNymvD4Y,5880
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=0Tph7T9D2XTLdTDoIpR2PcLTrvPt6h2LPuKowTXrDgU,5742
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=AQKCujjNRArzzmMP89eHnwypRLVR67A9PBvIkazcebo,16472
19
- producteca/sales_orders/search_sale_orders.py,sha256=oVegsfQ1vVrydyBbgbvc30TgQaufCnyEJfm8hOWWFfs,5022
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-2.0.33.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
29
- producteca-2.0.33.dist-info/METADATA,sha256=czmxx5JQJFyxSAYgUUN7RieVrHgtKcx9x6vqaztGsHQ,3809
30
- producteca-2.0.33.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
31
- producteca-2.0.33.dist-info/entry_points.txt,sha256=BFSDFLbB70p8YVZPiU7HDJdj2VyyLdMVa4ekLQAUAVc,125
32
- producteca-2.0.33.dist-info/RECORD,,
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,,