producteca 1.0.0__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/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from . import products
2
+ from . import search
3
+ from . import sales_orders
4
+ from . import shipments
5
+ from . import payments
File without changes
@@ -0,0 +1,23 @@
1
+ from pydantic import BaseModel, PrivateAttr
2
+ from abc import ABC, abstractmethod
3
+ from ..config.config import ConfigProducteca, APIConfig
4
+
5
+
6
+ class AbstractProductecaModel(BaseModel, ABC):
7
+ _config: APIConfig = PrivateAttr()
8
+
9
+ @property
10
+ @abstractmethod
11
+ def endpoint(self) -> str:
12
+ pass
13
+
14
+ def dict(self, *args, **kwargs):
15
+ return super().dict(*args, exclude_none=True, **kwargs)
16
+
17
+
18
+ class AbstractProductecaV1Model(AbstractProductecaModel):
19
+ _config: ConfigProducteca = PrivateAttr()
20
+
21
+ @property
22
+ def endpoint_url(self) -> str:
23
+ return self._config.get_endpoint(self.endpoint)
@@ -0,0 +1 @@
1
+ from .config import ConfigProducteca
@@ -0,0 +1,28 @@
1
+ from pydantic import BaseModel, PrivateAttr
2
+ from abc import ABC, abstractmethod
3
+
4
+
5
+ class APIConfig(BaseModel, ABC):
6
+ base_url: str = 'https://api-external.producteca.com'
7
+
8
+ @property
9
+ @abstractmethod
10
+ def headers(self) -> dict:
11
+ pass
12
+
13
+ def get_endpoint(self, endpoint: str) -> str:
14
+ return f'{self.base_url}/{endpoint}'
15
+
16
+
17
+ class ConfigProducteca(APIConfig):
18
+ token: str
19
+ api_key: str
20
+
21
+ @property
22
+ def headers(self) -> dict:
23
+ return {
24
+ "Content-Type": "application/json",
25
+ "authorization": f"Bearer {self.token}",
26
+ "x-api-key": self.api_key,
27
+ "Accept": "*/*"
28
+ }
File without changes
@@ -0,0 +1,45 @@
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+ import requests
4
+ from ..config.config import ConfigProducteca
5
+
6
+
7
+ class PaymentCard(BaseModel):
8
+ paymentNetwork: Optional[str] = None
9
+ firstSixDigits: Optional[int] = None
10
+ lastFourDigits: Optional[int] = None
11
+ cardholderIdentificationNumber: Optional[str] = None
12
+ cardholderIdentificationType: Optional[str] = None
13
+ cardholderName: Optional[str] = None
14
+
15
+
16
+ class PaymentIntegration(BaseModel):
17
+ integrationId: str
18
+ app: int
19
+
20
+
21
+ class Payment(BaseModel):
22
+ date: str
23
+ amount: float
24
+ couponAmount: Optional[float] = None
25
+ status: str
26
+ method: str
27
+ integration: Optional[PaymentIntegration] = None
28
+ transactionFee: Optional[float] = None
29
+ installments: Optional[int] = None
30
+ card: Optional[PaymentCard] = None
31
+ notes: Optional[str] = None
32
+ hasCancelableStatus: bool
33
+ id: Optional[int] = None
34
+
35
+ @classmethod
36
+ def create(cls, config: ConfigProducteca, sale_order_id: int, payload: "Payment") -> "Payment":
37
+ url = config.get_endpoint(f"salesorders/{sale_order_id}/payments")
38
+ res = requests.post(url, data=payload.model_dump_json(exclude_none=True), headers=config.headers)
39
+ return cls(**res.json())
40
+
41
+ @classmethod
42
+ def update(cls, config: ConfigProducteca, sale_order_id: int, payment_id: int, payload: "Payment") -> "Payment":
43
+ url = config.get_endpoint(f"salesorders/{sale_order_id}/payments/{payment_id}")
44
+ res = requests.put(url, data=payload.model_dump_json(exclude_none=True), headers=config.headers)
45
+ return cls(**res.json())
@@ -0,0 +1 @@
1
+ from . import products
@@ -0,0 +1,182 @@
1
+ from typing import List, Optional, Union
2
+ from pydantic import BaseModel, Field
3
+ import requests
4
+ from ..config.config import ConfigProducteca
5
+ import logging
6
+
7
+ _logger = logging.getLogger(__name__)
8
+
9
+ # Models for nested structures
10
+
11
+ class Attribute(BaseModel):
12
+ key: str
13
+ value: str
14
+
15
+ class Tag(BaseModel):
16
+ tag: str
17
+
18
+ class Dimensions(BaseModel):
19
+ weight: Optional[float] = None
20
+ width: Optional[float] = None
21
+ height: Optional[float] = None
22
+ length: Optional[float] = None
23
+ pieces: Optional[int] = None
24
+
25
+ class Deal(BaseModel):
26
+ campaign: str
27
+ regular_price: Optional[float] = Field(default=None, alias='regularPrice')
28
+ deal_price: Optional[float] = Field(default=None, alias='dealPrice')
29
+
30
+ class Stock(BaseModel):
31
+ quantity: Optional[int] = None
32
+ available_quantity: Optional[int] = Field(default=None, alias='availableQuantity')
33
+ warehouse: Optional[str] = None
34
+ warehouse_id: Optional[int] = Field(default=None, alias='warehouseId')
35
+ reserved: Optional[int] = None
36
+ available: Optional[int] = None
37
+
38
+ class Price(BaseModel):
39
+ amount: Optional[float] = None
40
+ currency: str
41
+ price_list: str = Field(alias='priceList')
42
+ price_list_id: Optional[int] = Field(default=None, alias='priceListId')
43
+
44
+ class Picture(BaseModel):
45
+ url: str
46
+
47
+ class Integration(BaseModel):
48
+ app: Optional[int] = None
49
+ integration_id: Optional[str] = Field(default=None, alias='integrationId')
50
+ permalink: Optional[str] = None
51
+ status: Optional[str] = None
52
+ listing_type: Optional[str] = Field(default=None, alias='listingType')
53
+ safety_stock: Optional[int] = Field(default=None, alias='safetyStock')
54
+ synchronize_stock: Optional[bool] = Field(default=None, alias='synchronizeStock')
55
+ is_active: Optional[bool] = Field(default=None, alias='isActive')
56
+ is_active_or_paused: Optional[bool] = Field(default=None, alias='isActiveOrPaused')
57
+ id: Optional[int] = None
58
+ parent_integration: Optional[str] = Field(default=None, alias='parentIntegration')
59
+
60
+ class Variation(BaseModel):
61
+ variation_id: Optional[int] = Field(default=None, alias='variationId')
62
+ components: Optional[List] = None
63
+ pictures: Optional[List[Picture]] = None
64
+ stocks: Optional[List[Stock]] = None
65
+ attributes_hash: Optional[str] = Field(default=None, alias='attributesHash')
66
+ primary_color: Optional[str] = Field(default=None, alias='primaryColor')
67
+ thumbnail: Optional[str] = None
68
+ attributes: Optional[List[Attribute]] = None
69
+ integrations: Optional[List[Integration]] = None
70
+ id: Optional[int] = None
71
+ sku: Optional[str] = None
72
+ barcode: Optional[str] = None
73
+
74
+ # Model base para los productos
75
+ class Product(BaseModel):
76
+ config: Optional[ConfigProducteca] = Field(default=None, exclude=True)
77
+ endpoint: str = Field(default='products', exclude=True)
78
+ create_if_it_doesnt_exist: bool = Field(default=False, exclude=True)
79
+ sku: Optional[str] = None
80
+ variation_id: Optional[int] = None
81
+ code: Optional[str] = None
82
+ name: Optional[str] = None
83
+ barcode: Optional[str] = None
84
+ attributes: Optional[List[Attribute]] = None
85
+ tags: Optional[List[str]] = None
86
+ buying_price: Optional[float] = None
87
+ dimensions: Optional[Dimensions] = None
88
+ category: Optional[Union[str, dict]] = None # Puede ser string en POST o dict en GET Meli
89
+ brand: Optional[str] = None
90
+ notes: Optional[str] = None
91
+ deals: Optional[List[Deal]] = None
92
+ stocks: Optional[List[Stock]] = None
93
+ prices: Optional[List[Price]] = None
94
+ pictures: Optional[List[Picture]] = None
95
+
96
+
97
+ def create(self):
98
+ endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
99
+ headers = self.config.headers.copy()
100
+ headers.update({"createifitdoesntexist": str(self.create_if_it_doesnt_exist).lower()})
101
+ data = self.model_dump_json(by_alias=True, exclude_none=True)
102
+ response = requests.post(endpoint_url, data=data, headers=headers)
103
+ if response.status_code == 204:
104
+ final_response = {"Message":"Product does not exist and the request cant create if it does not exist"}
105
+ else:
106
+ final_response = response.json()
107
+ return final_response, response.status_code
108
+
109
+ def update(self):
110
+ endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
111
+ headers = self.config.headers.copy()
112
+ data = self.model_dump_json(by_alias=True, exclude_none=True)
113
+ if not self.code and not self.sku:
114
+ return {"Message":"Sku or code should be provided to update the product"}, 204
115
+ response = requests.post(endpoint_url, data=data, headers=headers)
116
+ if response.status_code == 204:
117
+ final_response = {"Message":"Product does not exist and the request cant create if it does not exist"}
118
+ else:
119
+ final_response = response.json()
120
+ return final_response, response.status_code
121
+
122
+ @classmethod
123
+ def get(cls, config: ConfigProducteca, product_id: int):
124
+ endpoint_url = config.get_endpoint(f'{cls().endpoint}/{product_id}')
125
+ headers = config.headers
126
+ response = requests.get(endpoint_url, headers=headers)
127
+ response_data = response.json()
128
+ return response_data, response.status_code
129
+
130
+ @classmethod
131
+ def get_bundle(cls, config: ConfigProducteca, product_id: int):
132
+ endpoint_url = config.get_endpoint(f'{cls().endpoint}/{product_id}/bundles')
133
+ headers = config.headers
134
+ response = requests.get(endpoint_url, headers=headers)
135
+ return cls(config=config, **response.json()), response.status_code
136
+
137
+ @classmethod
138
+ def get_ml_integration(cls, config: ConfigProducteca, product_id: int):
139
+ endpoint_url = config.get_endpoint(f'{cls().endpoint}/{product_id}/listintegration')
140
+ headers = config.headers
141
+ response = requests.get(endpoint_url, headers=headers)
142
+ return cls(config=config, **response.json()), response.status_code
143
+
144
+ # Modelo con campos extra de la vista Meli
145
+ class MeliCategory(BaseModel):
146
+ meli_id: Optional[str] = Field(default=None, alias='meliId')
147
+ accepts_mercadoenvios: Optional[bool] = Field(default=None, alias='acceptsMercadoenvios')
148
+ suggest: Optional[bool] = None
149
+ fixed: Optional[bool] = None
150
+
151
+ class Shipping(BaseModel):
152
+ local_pickup: Optional[bool] = Field(default=None, alias='localPickup')
153
+ mode: Optional[str] = None
154
+ free_shipping: Optional[bool] = Field(default=None, alias='freeShipping')
155
+ free_shipping_cost: Optional[float] = Field(default=None, alias='freeShippingCost')
156
+ mandatory_free_shipping: Optional[bool] = Field(default=None, alias='mandatoryFreeShipping')
157
+ free_shipping_method: Optional[str] = Field(default=None, alias='freeShippingMethod')
158
+
159
+ class MShopsShipping(BaseModel):
160
+ enabled: Optional[bool] = None
161
+
162
+ class AttributeCompletion(BaseModel):
163
+ product_identifier_status: Optional[str] = Field(default=None, alias='productIdentifierStatus')
164
+ data_sheet_status: Optional[str] = Field(default=None, alias='dataSheetStatus')
165
+ status: Optional[str] = None
166
+ count: Optional[int] = None
167
+ total: Optional[int] = None
168
+
169
+ class MeliProduct(Product):
170
+ product_id: Optional[int] = Field(default=None, alias='productId')
171
+ has_custom_shipping_costs: Optional[bool] = Field(default=None, alias='hasCustomShippingCosts')
172
+ shipping: Optional[Shipping] = None
173
+ mshops_shipping: Optional[MShopsShipping] = Field(default=None, alias='mShopsShipping')
174
+ add_free_shipping_cost_to_price: Optional[bool] = Field(default=None, alias='addFreeShippingCostToPrice')
175
+ category: Optional[MeliCategory] = None
176
+ attribute_completion: Optional[AttributeCompletion] = Field(default=None, alias='attributeCompletion')
177
+ catalog_products: Optional[List[str]] = Field(default=None, alias='catalogProducts')
178
+ warranty: Optional[str] = None
179
+ domain: Optional[str] = None
180
+ listing_type_id: Optional[str] = Field(default=None, alias='listingTypeId')
181
+ catalog_products_status: Optional[str] = Field(default=None, alias='catalogProductsStatus')
182
+
@@ -0,0 +1 @@
1
+ from . import sales_orders
@@ -0,0 +1,255 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional
3
+ import requests
4
+ from ..config.config import ConfigProducteca
5
+ import logging
6
+ _logger = logging.getLogger(__name__)
7
+
8
+ class SaleOrderLocation(BaseModel):
9
+ streetName: Optional[str] = None
10
+ streetNumber: Optional[str] = None
11
+ addressNotes: Optional[str] = None
12
+ state: Optional[str] = None
13
+ city: Optional[str] = None
14
+ neighborhood: Optional[str] = None
15
+ zipCode: Optional[str] = None
16
+
17
+ class SaleOrderBillingInfo(BaseModel):
18
+ docType: Optional[str] = None
19
+ docNumber: Optional[str] = None
20
+ streetName: Optional[str] = None
21
+ streetNumber: Optional[str] = None
22
+ comment: Optional[str] = None
23
+ zipCode: Optional[str] = None
24
+ city: Optional[str] = None
25
+ state: Optional[str] = None
26
+ stateRegistration: Optional[str] = None
27
+ taxPayerType: Optional[str] = None
28
+ firstName: Optional[str] = None
29
+ lastName: Optional[str] = None
30
+ businessName: Optional[str] = None
31
+
32
+ class SaleOrderProfile(BaseModel):
33
+ app: int
34
+ integrationId: str
35
+ nickname: Optional[str] = None
36
+
37
+ class SaleOrderContact(BaseModel):
38
+ id: int
39
+ name: str
40
+ contactPerson: Optional[str] = None
41
+ mail: Optional[str] = None
42
+ phoneNumber: Optional[str] = None
43
+ taxId: Optional[str] = None
44
+ location: Optional[SaleOrderLocation] = None
45
+ notes: Optional[str] = None
46
+ type: Optional[str] = None
47
+ priceList: Optional[str] = None
48
+ priceListId: Optional[str] = None
49
+ profile: Optional[SaleOrderProfile] = None
50
+ billingInfo: Optional[SaleOrderBillingInfo] = None
51
+
52
+ class SaleOrderIntegrationId(BaseModel):
53
+ alternateId: Optional[str] = None
54
+ integrationId: str
55
+ app: int
56
+
57
+ class SaleOrderVariationPicture(BaseModel):
58
+ url: str
59
+ id: Optional[int] = None
60
+
61
+ class SaleOrderVariationStock(BaseModel):
62
+ warehouseId: Optional[int] = None
63
+ warehouse: str
64
+ quantity: int
65
+ reserved: int
66
+ lastModified: Optional[str] = None
67
+ available: int
68
+
69
+ class SaleOrderVariationAttribute(BaseModel):
70
+ key: str
71
+ value: str
72
+
73
+ class SaleOrderVariation(BaseModel):
74
+ supplierCode: Optional[str] = None
75
+ pictures: Optional[List[SaleOrderVariationPicture]] = None
76
+ stocks: Optional[List[SaleOrderVariationStock]] = None
77
+ integrationId: Optional[int] = None
78
+ attributesHash: Optional[str] = None
79
+ primaryColor: Optional[str] = None
80
+ secondaryColor: Optional[str] = None
81
+ size: Optional[str] = None
82
+ thumbnail: Optional[str] = None
83
+ attributes: Optional[List[SaleOrderVariationAttribute]] = None
84
+ integrations: Optional[List[SaleOrderIntegrationId]] = None
85
+ id: int
86
+ sku: str
87
+ barcode: Optional[str] = None
88
+
89
+ class SaleOrderProduct(BaseModel):
90
+ name: str
91
+ code: str
92
+ brand: Optional[str] = None
93
+ id: int
94
+
95
+ class SaleOrderConversation(BaseModel):
96
+ questions: Optional[List[str]] = None
97
+
98
+ class SaleOrderLine(BaseModel):
99
+ price: float
100
+ originalPrice: Optional[float] = None
101
+ transactionFee: Optional[float] = None
102
+ product: SaleOrderProduct
103
+ variation: SaleOrderVariation
104
+ orderVariationIntegrationId: Optional[str] = None
105
+ quantity: int
106
+ conversation: Optional[SaleOrderConversation] = None
107
+ reserved: Optional[int] = None
108
+ id: int
109
+
110
+ class SaleOrderCard(BaseModel):
111
+ paymentNetwork: Optional[str] = None
112
+ firstSixDigits: Optional[int] = None
113
+ lastFourDigits: Optional[int] = None
114
+ cardholderIdentificationNumber: Optional[str] = None
115
+ cardholderIdentificationType: Optional[str] = None
116
+ cardholderName: Optional[str] = None
117
+
118
+ class SaleOrderPaymentIntegration(BaseModel):
119
+ integrationId: str
120
+ app: int
121
+
122
+ class SaleOrderPayment(BaseModel):
123
+ date: Optional[str] = None
124
+ amount: float
125
+ couponAmount: Optional[float] = None
126
+ status: Optional[str] = None
127
+ method: Optional[str] = None
128
+ integration: Optional[SaleOrderPaymentIntegration] = None
129
+ transactionFee: Optional[float] = None
130
+ installments: Optional[int] = None
131
+ card: Optional[SaleOrderCard] = None
132
+ notes: Optional[str] = None
133
+ authorizationCode: Optional[str] = None
134
+ hasCancelableStatus: Optional[bool] = None
135
+ id: Optional[int] = None
136
+
137
+ class SaleOrderShipmentMethod(BaseModel):
138
+ trackingNumber: Optional[str] = None
139
+ trackingUrl: Optional[str] = None
140
+ courier: Optional[str] = None
141
+ mode: Optional[str] = None
142
+ cost: Optional[float] = None
143
+ type: Optional[str] = None
144
+ eta: Optional[int] = None
145
+ status: Optional[str] = None
146
+
147
+ class SaleOrderShipmentProduct(BaseModel):
148
+ product: int
149
+ variation: int
150
+ quantity: int
151
+
152
+ class SaleOrderShipmentIntegration(BaseModel):
153
+ app: int
154
+ integrationId: str
155
+ status: str
156
+ id: int
157
+
158
+ class SaleOrderShipment(BaseModel):
159
+ date: str
160
+ products: List[SaleOrderShipmentProduct]
161
+ method: Optional[SaleOrderShipmentMethod] = None
162
+ integration: Optional[SaleOrderShipmentIntegration] = None
163
+ receiver: Optional[dict] = None
164
+ id: int
165
+
166
+ class SaleOrderInvoiceIntegration(BaseModel):
167
+ id: Optional[int] = None
168
+ integrationId: Optional[str] = None
169
+ app: Optional[int] = None
170
+ createdAt: Optional[str] = None
171
+ documentUrl: Optional[str] = None
172
+ xmlUrl: Optional[str] = None
173
+ decreaseStock: Optional[bool] = None
174
+
175
+ class SaleOrder(BaseModel):
176
+ tags: Optional[List[str]] = None
177
+ integrations: Optional[List[SaleOrderIntegrationId]] = None
178
+ invoiceIntegration: Optional[SaleOrderInvoiceIntegration] = None
179
+ channel: Optional[str] = None
180
+ piiExpired: Optional[bool] = None
181
+ contact: Optional[SaleOrderContact] = None
182
+ lines: Optional[List[SaleOrderLine]] = None
183
+ warehouse: Optional[str] = None
184
+ warehouseId: Optional[int] = None
185
+ warehouseIntegration: Optional[str] = None
186
+ pickUpStore: Optional[str] = None
187
+ payments: Optional[List[SaleOrderPayment]] = None
188
+ shipments: Optional[List[SaleOrderShipment]] = None
189
+ amount: Optional[float] = None
190
+ shippingCost: Optional[float] = None
191
+ financialCost: Optional[float] = None
192
+ paidApproved: Optional[float] = None
193
+ paymentStatus: Optional[str] = None
194
+ deliveryStatus: Optional[str] = None
195
+ paymentFulfillmentStatus: Optional[str] = None
196
+ deliveryFulfillmentStatus: Optional[str] = None
197
+ deliveryMethod: Optional[str] = None
198
+ paymentTerm: Optional[str] = None
199
+ currency: Optional[str] = None
200
+ customId: Optional[str] = None
201
+ isOpen: Optional[bool] = None
202
+ isCanceled: Optional[bool] = None
203
+ cartId: Optional[str] = None
204
+ draft: Optional[bool] = None
205
+ promiseDeliveryDate: Optional[str] = None
206
+ promiseDispatchDate: Optional[str] = None
207
+ hasAnyShipments: Optional[bool] = None
208
+ hasAnyPayments: Optional[bool] = None
209
+ date: Optional[str] = None
210
+ notes: Optional[str] = None
211
+ id: int
212
+
213
+ @classmethod
214
+ def get(cls, config: ConfigProducteca, sale_order_id: int) -> "SaleOrder":
215
+ endpoint = f'salesorders/{sale_order_id}'
216
+ url = config.get_endpoint(endpoint)
217
+ response = requests.get(url, headers=config.headers)
218
+ return cls(**response.json())
219
+
220
+ @classmethod
221
+ def get_shipping_labels(cls, config: ConfigProducteca, sale_order_id: int):
222
+ endpoint = f'salesorders/{sale_order_id}/labels'
223
+ url = config.get_endpoint(endpoint)
224
+ response = requests.get(url, headers=config.headers)
225
+ return response.json()
226
+
227
+ @classmethod
228
+ def close(cls, config: ConfigProducteca, sale_order_id: int):
229
+ endpoint = f'salesorders/{sale_order_id}/close'
230
+ url = config.get_endpoint(endpoint)
231
+ response = requests.post(url, headers=config.headers)
232
+ return response.status_code, response.json()
233
+
234
+ @classmethod
235
+ def cancel(cls, config: ConfigProducteca, sale_order_id: int):
236
+ endpoint = f'salesorders/{sale_order_id}/cancel'
237
+ url = config.get_endpoint(endpoint)
238
+ response = requests.post(url, headers=config.headers)
239
+ return response.status_code, response.json()
240
+
241
+ @classmethod
242
+ def synchronize(cls, config: ConfigProducteca, payload: "SaleOrder") -> tuple[int, "SaleOrder"]:
243
+ endpoint = 'salesorders/synchronize'
244
+ url = config.get_endpoint(endpoint)
245
+ response = requests.post(url, data=payload.model_dump_json(exclude_none=True), headers=config.headers)
246
+ return response.status_code, cls(**response.json())
247
+
248
+ @classmethod
249
+ def invoice_integration(cls, config: ConfigProducteca, sale_order_id: int, payload: "SaleOrder"):
250
+ endpoint = f'salesorders/{sale_order_id}/invoiceIntegration'
251
+ url = config.get_endpoint(endpoint)
252
+ response = requests.put(url, headers=config.headers, data=payload.model_dump_json(exclude_none=True))
253
+ if response.status_code == 200:
254
+ return response.status_code, {}
255
+ return response.status_code, response.json()
File without changes
@@ -0,0 +1,149 @@
1
+ from typing import List, Optional
2
+ from pydantic import BaseModel, Field
3
+ import requests
4
+ from ..config.config import ConfigProducteca
5
+
6
+
7
+ class FacetValue(BaseModel):
8
+ count: int
9
+ value: str
10
+ label: str
11
+
12
+
13
+ class Facet(BaseModel):
14
+ key: str
15
+ value: List[FacetValue]
16
+ is_collection: bool
17
+ translate: bool
18
+
19
+
20
+ class SearchStocks(BaseModel):
21
+ warehouse: str
22
+ quantity: int
23
+ reserved: int
24
+
25
+
26
+ class SearchPrices(BaseModel):
27
+ price_list_id: int
28
+ price_list: str
29
+ amount: float
30
+ currency: str
31
+
32
+
33
+ class SearchIntegration(BaseModel):
34
+ app: Optional[int]
35
+ integration_id: Optional[str]
36
+ permalink: Optional[str]
37
+ status: Optional[str]
38
+ listing_type: Optional[str]
39
+ safety_stock: Optional[int]
40
+ synchronize_stock: Optional[bool]
41
+ is_active: Optional[bool]
42
+ is_active_or_paused: Optional[bool]
43
+ id: Optional[int]
44
+
45
+
46
+ class SearchDeals(BaseModel):
47
+ campaign: str
48
+ product: int
49
+ variation: str
50
+ deal_price: float
51
+ discount: float
52
+ regular_price: float
53
+ enabled: bool
54
+ currency: str
55
+ id: str
56
+
57
+
58
+ class SearchResultItem(BaseModel):
59
+ search_score: float = Field(..., alias='@search.score')
60
+ id: int
61
+ product_id: int
62
+ company_id: int
63
+ name: str
64
+ code: str
65
+ skus: List[str]
66
+ brand: str
67
+ category: str
68
+ thumbnail: str
69
+ stocks: List[SearchStocks]
70
+ warehouses_with_stock: List[str]
71
+ total_stock: int
72
+ has_pictures: bool
73
+ buying_price: float
74
+ prices: List[SearchPrices]
75
+ integration_ids: List[str]
76
+ integration_apps: List[str]
77
+ integrations: List[SearchIntegration]
78
+ campaigns: List[str]
79
+ app: Optional[int]
80
+ status: Optional[str]
81
+ synchronize_stock: Optional[bool]
82
+ listing_type: Optional[str]
83
+ price_amount: Optional[float]
84
+ price_currency: Optional[str]
85
+ category_id: Optional[str]
86
+ category_base_id: Optional[str]
87
+ category_l1: Optional[str]
88
+ category_l2: Optional[str]
89
+ category_l3: Optional[str]
90
+ category_l4: Optional[str]
91
+ category_l5: Optional[str]
92
+ category_l6: Optional[str]
93
+ has_category: Optional[bool]
94
+ category_fixed: Optional[bool]
95
+ accepts_mercadoenvios: Optional[bool]
96
+ shipping_mode: Optional[str]
97
+ local_pickup: Optional[bool]
98
+ mandatory_free_shipping: Optional[bool]
99
+ free_shipping: Optional[bool]
100
+ free_shipping_cost: Optional[float]
101
+ template: Optional[str]
102
+ youtube_id: Optional[str]
103
+ warranty: Optional[str]
104
+ permalink: Optional[str]
105
+ domain: Optional[str]
106
+ attribute_completion_status: Optional[str]
107
+ attribute_completion_count: Optional[int]
108
+ attribute_completion_total: Optional[int]
109
+ deals: Optional[SearchDeals]
110
+ campaign_status: Optional[List[str]]
111
+ size_chart: Optional[str]
112
+ channel_status: Optional[List[str]]
113
+ channel_category_l1: Optional[List[str]]
114
+ channel_category_l2: Optional[List[str]]
115
+ channel_category_l3: Optional[List[str]]
116
+ channel_category_id: Optional[List[str]]
117
+ channel_synchronizes_stock: Optional[List[str]]
118
+ channel_has_category: Optional[List[str]]
119
+ catalog_products_status: Optional[List[str]]
120
+ metadata: Optional[List[str]]
121
+ integration_tags: Optional[List[str]]
122
+ variations_integration_ids: Optional[List[str]]
123
+ channel_pictures_templates: Optional[List[str]]
124
+ channel_pictures_templates_apps: Optional[List[str]]
125
+
126
+
127
+ class SearchProductResponse(BaseModel):
128
+ count: int
129
+ facets: List[Facet]
130
+ results: List[SearchResultItem]
131
+
132
+
133
+ class SearchProductParams(BaseModel):
134
+ top: Optional[int]
135
+ skip: Optional[int]
136
+ filter: Optional[str] = Field(default=None, alias='$filter')
137
+ search: Optional[str]
138
+ sales_channel: Optional[str] = Field(default='2', alias='salesChannel')
139
+
140
+
141
+ class SearchProduct:
142
+ endpoint: str = 'search/products'
143
+
144
+ @classmethod
145
+ def search_product(cls, config: ConfigProducteca, params: SearchProductParams) -> SearchProductResponse:
146
+ headers = config.headers
147
+ url = config.get_endpoint(cls.endpoint)
148
+ response = requests.get(url, headers=headers, params=params.dict(by_alias=True, exclude_none=True))
149
+ return SearchProductResponse(**response.json())
@@ -0,0 +1,170 @@
1
+ from typing import List, Optional
2
+ from pydantic import BaseModel, Field
3
+ import requests
4
+ from ..config.config import ConfigProducteca
5
+ import logging
6
+
7
+ _logger = logging.getLogger(__name__)
8
+
9
+
10
+ class SalesOrderProduct(BaseModel):
11
+ id: int
12
+ name: str
13
+ code: str
14
+ brand: str
15
+
16
+
17
+ class SalesOrderVariationAttribute(BaseModel):
18
+ key: str
19
+ value: str
20
+
21
+
22
+ class SalesOrderVariation(BaseModel):
23
+ id: int
24
+ attributes: List[SalesOrderVariationAttribute]
25
+ sku: str
26
+ thumbnail: str
27
+
28
+
29
+ class SalesOrderLine(BaseModel):
30
+ product: SalesOrderProduct
31
+ variation: SalesOrderVariation
32
+ quantity: int
33
+ price: float
34
+
35
+
36
+ class SalesOrderCard(BaseModel):
37
+ payment_network: str
38
+ first_six_digits: int
39
+ last_four_digits: int
40
+ cardholder_identification_number: str
41
+ cardholder_identification_type: str
42
+ cardholder_name: str
43
+
44
+
45
+ class SalesOrderPaymentIntegration(BaseModel):
46
+ integration_id: str
47
+ app: int
48
+
49
+
50
+ class SalesOrderPayment(BaseModel):
51
+ date: str
52
+ amount: float
53
+ coupon_amount: float
54
+ status: str
55
+ method: str
56
+ integration: SalesOrderPaymentIntegration
57
+ transaction_fee: float
58
+ installments: int
59
+ card: SalesOrderCard
60
+ notes: str
61
+ has_cancelable_status: bool
62
+ id: int
63
+
64
+
65
+ class SalesOrderIntegration(BaseModel):
66
+ alternate_id: str
67
+ integration_id: int
68
+ app: int
69
+
70
+
71
+ class SalesOrderShipmentProduct(BaseModel):
72
+ product: int
73
+ variation: int
74
+ quantity: int
75
+
76
+
77
+ class SalesOrderShipmentMethod(BaseModel):
78
+ tracking_number: str
79
+ tracking_url: str
80
+ courier: str
81
+ mode: str
82
+ cost: float
83
+ type: str
84
+ eta: str
85
+ status: str
86
+
87
+
88
+ class SalesOrderShipmentIntegration(BaseModel):
89
+ id: int
90
+ integration_id: str
91
+ app: int
92
+ status: str
93
+
94
+
95
+ class SalesOrderShipment(BaseModel):
96
+ date: str
97
+ products: List[SalesOrderShipmentProduct]
98
+ method: SalesOrderShipmentMethod
99
+ integration: SalesOrderShipmentIntegration
100
+
101
+
102
+ class SalesOrderResultItem(BaseModel):
103
+ codes: List[str]
104
+ contact_id: int
105
+ currency: str
106
+ date: str
107
+ delivery_method: str
108
+ delivery_status: str
109
+ id: str
110
+ integration_ids: List[str]
111
+ integrations: List[SalesOrderIntegration]
112
+ invoice_integration_app: int
113
+ invoice_integration_id: str
114
+ lines: List[SalesOrderLine]
115
+ payments: List[SalesOrderPayment]
116
+ payment_status: str
117
+ payment_term: str
118
+ product_names: List[str]
119
+ reserving_product_ids: str
120
+ sales_channel: int
121
+ shipments: List[SalesOrderShipment]
122
+ tracking_number: str
123
+ skus: List[str]
124
+ status: str
125
+ tags: List[str]
126
+ warehouse: str
127
+ company_id: int
128
+ shipping_cost: float
129
+ contact_phone: str
130
+ brands: List[str]
131
+ courier: str
132
+ order_id: int
133
+ updated_at: str
134
+ invoice_integration_created_at: str
135
+ invoice_integration_document_url: str
136
+ has_document_url: bool
137
+ integration_alternate_ids: str
138
+ cart_id: str
139
+ amount: float
140
+ has_any_shipments: bool
141
+
142
+
143
+ class SearchSalesOrderResponse(BaseModel):
144
+ count: int
145
+ results: List[SalesOrderResultItem]
146
+
147
+
148
+ class SearchSalesOrderParams(BaseModel):
149
+ top: Optional[int]
150
+ skip: Optional[int]
151
+ filter: Optional[str] = Field(default=None, alias="$filter")
152
+ class Config:
153
+ validate_by_name = True
154
+
155
+ class SearchSalesOrder:
156
+ endpoint: str = "search/salesorders"
157
+
158
+
159
+ @classmethod
160
+ def search_saleorder(cls, config: ConfigProducteca, params: SearchSalesOrderParams):
161
+ headers = config.headers
162
+ url = config.get_endpoint(cls.endpoint)
163
+ new_url = f"{url}?$filter={params.filter}&top={params.top}&skip={params.skip}"
164
+ response = requests.get(
165
+ new_url,
166
+ headers=headers,
167
+ )
168
+ return response.json(), response.status_code
169
+
170
+
@@ -0,0 +1 @@
1
+ from . import shipment
@@ -0,0 +1,47 @@
1
+ from typing import List, Optional
2
+ from pydantic import BaseModel
3
+ import requests
4
+ from ..config.config import ConfigProducteca
5
+
6
+
7
+ class ShipmentProduct(BaseModel):
8
+ product: int
9
+ variation: Optional[int] = None
10
+ quantity: int
11
+
12
+
13
+ class ShipmentMethod(BaseModel):
14
+ trackingNumber: Optional[str] = None
15
+ trackingUrl: Optional[str] = None
16
+ courier: Optional[str] = None
17
+ mode: Optional[str] = None
18
+ cost: Optional[float] = None
19
+ type: Optional[str] = None
20
+ eta: Optional[int] = None
21
+ status: Optional[str] = None
22
+
23
+
24
+ class ShipmentIntegration(BaseModel):
25
+ id: Optional[int] = None
26
+ integrationId: Optional[str] = None
27
+ app: Optional[int] = None
28
+ status: str
29
+
30
+
31
+ class Shipment(BaseModel):
32
+ date: Optional[str] = None
33
+ products: Optional[List[ShipmentProduct]] = None
34
+ method: Optional[ShipmentMethod] = None
35
+ integration: Optional[ShipmentIntegration] = None
36
+
37
+ @classmethod
38
+ def create(cls, config: ConfigProducteca, sale_order_id: int, payload: "Shipment") -> "Shipment":
39
+ url = config.get_endpoint(f"salesorders/{sale_order_id}/shipments")
40
+ res = requests.post(url, data=payload.model_dump_json(exclude_none=True), headers=config.headers)
41
+ return res.status_code, res.json()
42
+
43
+ @classmethod
44
+ def update(cls, config: ConfigProducteca, sale_order_id: int, shipment_id: str, payload: "Shipment") -> "Shipment":
45
+ url = config.get_endpoint(f"salesorders/{sale_order_id}/shipments/{shipment_id}")
46
+ res = requests.put(url, data=payload.model_dump_json(exclude_none=True), headers=config.headers)
47
+ return res.status_code, res.json()
@@ -0,0 +1,55 @@
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock
3
+ from .shipment import Shipment, ShipmentProduct, ShipmentMethod, ShipmentIntegration, ConfigProducteca
4
+
5
+ class TestShipment(unittest.TestCase):
6
+
7
+ @patch('requests.post')
8
+ def test_create_shipment(self, mock_post):
9
+ # Arrange
10
+ config = ConfigProducteca()
11
+ sale_order_id = 123
12
+ products = [ShipmentProduct(product=1, variation=2, quantity=3)]
13
+ method = ShipmentMethod(trackingNumber="TN123", trackingUrl="http://track.url", courier="DHL", mode="air", cost=10.5, type="express", eta=5, status="shipped")
14
+ integration = ShipmentIntegration(id=1, integrationId="int123", app=10, status="active")
15
+ payload = Shipment(date="2023-01-01", products=products, method=method, integration=integration)
16
+
17
+ mock_response = MagicMock()
18
+ mock_response.status_code = 201
19
+ mock_response.json.return_value = {'success': True}
20
+ mock_post.return_value = mock_response
21
+
22
+ # Act
23
+ status_code, response_json = Shipment.create(config, sale_order_id, payload)
24
+
25
+ # Assert
26
+ self.assertEqual(status_code, 201)
27
+ self.assertEqual(response_json, {'success': True})
28
+ mock_post.assert_called_once()
29
+
30
+ @patch('requests.put')
31
+ def test_update_shipment(self, mock_put):
32
+ # Arrange
33
+ config = ConfigProducteca()
34
+ sale_order_id = 123
35
+ shipment_id = 'abc'
36
+ products = [ShipmentProduct(product=4, quantity=7)]
37
+ method = ShipmentMethod(courier="FedEx", cost=15.0)
38
+ integration = ShipmentIntegration(status="pending")
39
+ payload = Shipment(date="2023-02-02", products=products, method=method, integration=integration)
40
+
41
+ mock_response = MagicMock()
42
+ mock_response.status_code = 200
43
+ mock_response.json.return_value = {'updated': True}
44
+ mock_put.return_value = mock_response
45
+
46
+ # Act
47
+ status_code, response_json = Shipment.update(config, sale_order_id, shipment_id, payload)
48
+
49
+ # Assert
50
+ self.assertEqual(status_code, 200)
51
+ self.assertEqual(response_json, {'updated': True})
52
+ mock_put.assert_called_once()
53
+
54
+ if __name__ == '__main__':
55
+ unittest.main()
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.3
2
+ Name: producteca
3
+ Version: 1.0.0
4
+ Summary:
5
+ Author: Chroma Agency, Matias Rivera
6
+ Author-email: mrivera@chroma.agency
7
+ Requires-Python: >=3.13,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Requires-Dist: coverage (>=7.6.3,<8.0.0)
11
+ Requires-Dist: pydantic (>=2.11.7,<3.0.0)
12
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Very small module to connect with producteca
@@ -0,0 +1,21 @@
1
+ producteca/__init__.py,sha256=pX_QNJAeP-LJaRdArww3O8MiOp2yTAX1OCjzvzAjU3E,118
2
+ producteca/abstract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ producteca/abstract/abstract_dataclass.py,sha256=Q9BaVb34_2MBLWaOOeGuzd1diOePHxxFWBEr8Jw8t40,617
4
+ producteca/config/__init__.py,sha256=ZELnRKNOj0erqtCLRtZURqUhttVbFfqUrHrgg6M5p-M,36
5
+ producteca/config/config.py,sha256=uTRaHLI9L7P_LPuFmuYgV50Aedz9MfDbXOZVZPFkOuI,658
6
+ producteca/payments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ producteca/payments/payments.py,sha256=bc9556954GRsvm4XAsTbbdP4-rS275MWGfhuF4MYVsY,1594
8
+ producteca/products/__init__.py,sha256=kIkYF7_BcLUzkGvuRnUBPAELyG9QxUb5D6UiRg4XQCg,22
9
+ producteca/products/products.py,sha256=IagCPEvQBUuUaw9yBQwBu7eJw3HtdAjxiWHEnpLDXwM,8046
10
+ producteca/sales_orders/__init__.py,sha256=9NbTrJhvhtGYX2DFd7CCZvYVKXx6jJCV2L70XPma5_c,26
11
+ producteca/sales_orders/sales_orders.py,sha256=1VzFSou2wOEmZ-uhehVcsj9Q4Gk5Iam4YEIu-l-xEgc,8792
12
+ producteca/search/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ producteca/search/search.py,sha256=4u_Y5h6ffxxD4_iaSVpKv6ZIOoxU_vZ2cuHvygclmRw,4137
14
+ producteca/search/search_sale_orders.py,sha256=JXxohAhEokUQ_bxBIQesLO_FzpN35lKfpT3fB8-O_Fs,3724
15
+ producteca/shipments/__init__.py,sha256=YZTSiaXNQHTtTrYjnl2bn_E6mcQyS76XVbB7fpCEC2Q,22
16
+ producteca/shipments/shipment.py,sha256=IELAYWVNT862CBvDHOTotqLMXrm6sFECic2E7RTSI-A,1625
17
+ producteca/shipments/test_shipment.py,sha256=QBi6Ee4UcLQgiVLPz3sfug5bZ5F655UTomde0xnDfKk,2215
18
+ producteca-1.0.0.dist-info/METADATA,sha256=ISMhh36r2JSZEHDclGbdjrPeLDmFeHxc5mp6Vz5D83s,478
19
+ producteca-1.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
20
+ producteca-1.0.0.dist-info/entry_points.txt,sha256=Fk7pn09epcdztiFc7kuYBBlB_e6R2C_c1hmPHsookHI,143
21
+ producteca-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ generate_html_coverage=scripts:generate_html_coverage
3
+ get_coverage=scripts:get_coverage
4
+ main=logseek:main
5
+ test=scripts:test
6
+