producteca 1.0.14__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/__init__.py +3 -0
- producteca/abstract/abstract_dataclass.py +20 -18
- producteca/client.py +8 -21
- producteca/config/__init__.py +0 -1
- producteca/payments/payments.py +4 -16
- producteca/payments/tests/test_payments.py +21 -5
- producteca/products/__init__.py +0 -1
- producteca/products/products.py +237 -81
- producteca/products/search_products.py +136 -0
- producteca/products/tests/test_products.py +111 -38
- producteca/{search/tests/test_search.py → products/tests/test_search_products.py} +15 -86
- producteca/sales_orders/__init__.py +0 -1
- producteca/sales_orders/sales_orders.py +292 -118
- producteca/sales_orders/search_sale_orders.py +152 -0
- producteca/sales_orders/tests/search.json +137 -0
- producteca/sales_orders/tests/test_sales_orders.py +55 -26
- producteca/sales_orders/tests/test_search_so.py +46 -0
- producteca/shipments/shipment.py +0 -14
- producteca/shipments/tests/test_shipment.py +28 -23
- producteca/utils.py +50 -0
- producteca-2.0.57.dist-info/LICENSE +661 -0
- {producteca-1.0.14.dist-info → producteca-2.0.57.dist-info}/METADATA +33 -2
- producteca-2.0.57.dist-info/RECORD +33 -0
- producteca/search/__init__.py +0 -0
- producteca/search/search.py +0 -149
- producteca/search/search_sale_orders.py +0 -170
- producteca/search/tests/__init__.py +0 -0
- producteca-1.0.14.dist-info/RECORD +0 -31
- {producteca-1.0.14.dist-info → producteca-2.0.57.dist-info}/WHEEL +0 -0
- {producteca-1.0.14.dist-info → producteca-2.0.57.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from typing import List, Optional, Union
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
_logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SalesOrderProduct(BaseModel):
|
|
9
|
+
id: int
|
|
10
|
+
name: str
|
|
11
|
+
code: Optional[str] = None
|
|
12
|
+
brand: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SalesOrderVariationAttribute(BaseModel):
|
|
16
|
+
key: Optional[str] = None
|
|
17
|
+
value: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SalesOrderVariation(BaseModel):
|
|
21
|
+
id: int
|
|
22
|
+
attributes: Optional[List[SalesOrderVariationAttribute]] = None
|
|
23
|
+
sku: Optional[str] = None
|
|
24
|
+
thumbnail: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SalesOrderLine(BaseModel):
|
|
28
|
+
product: SalesOrderProduct
|
|
29
|
+
variation: Optional[SalesOrderVariation] = None
|
|
30
|
+
quantity: int
|
|
31
|
+
price: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SalesOrderCard(BaseModel):
|
|
35
|
+
payment_network: str = Field(alias="paymentNetwork")
|
|
36
|
+
first_six_digits: int = Field(alias="firstSixDigits")
|
|
37
|
+
last_four_digits: int = Field(alias="lastFourDigits")
|
|
38
|
+
cardholder_identification_number: str = Field(alias="cardholderIdentificationNumber")
|
|
39
|
+
cardholder_identification_type: str = Field(alias="cardholderIdentificationType")
|
|
40
|
+
cardholder_name: str = Field(alias="cardholderName")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SalesOrderPaymentIntegration(BaseModel):
|
|
44
|
+
integration_id: str = Field(alias="integrationId")
|
|
45
|
+
app: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SalesOrderPayment(BaseModel):
|
|
49
|
+
date: str
|
|
50
|
+
amount: float
|
|
51
|
+
coupon_amount: Optional[float] = Field(default=None, alias="couponAmount")
|
|
52
|
+
status: Optional[str] = None
|
|
53
|
+
method: str
|
|
54
|
+
integration: Optional[SalesOrderPaymentIntegration] = None
|
|
55
|
+
transaction_fee: Optional[float] = Field(default=None, alias="transactionFee")
|
|
56
|
+
installments: Optional[int] = None
|
|
57
|
+
card: Optional[SalesOrderCard] = None
|
|
58
|
+
notes: Optional[str] = None
|
|
59
|
+
has_cancelable_status: Optional[bool] = Field(default=None, alias="hasCancelableStatus")
|
|
60
|
+
id: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SalesOrderIntegration(BaseModel):
|
|
64
|
+
alternate_id: Optional[str] = Field(default=None, alias="alternateId")
|
|
65
|
+
integration_id: Union[str, int] = Field(alias="integrationId")
|
|
66
|
+
app: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SalesOrderShipmentProduct(BaseModel):
|
|
70
|
+
product: int
|
|
71
|
+
variation: int
|
|
72
|
+
quantity: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SalesOrderShipmentMethod(BaseModel):
|
|
76
|
+
tracking_number: Optional[str] = Field(alias="trackingNumber")
|
|
77
|
+
tracking_url: Optional[str] = Field(alias="trackingUrl")
|
|
78
|
+
courier: Optional[str] = None
|
|
79
|
+
mode: Optional[str] = None
|
|
80
|
+
cost: Optional[float] = None
|
|
81
|
+
type: Optional[str] = None
|
|
82
|
+
eta: Optional[Union[int, str]] = Field(None)
|
|
83
|
+
status: Optional[str] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SalesOrderShipmentIntegration(BaseModel):
|
|
87
|
+
id: int
|
|
88
|
+
integration_id: str = Field(alias="integrationId")
|
|
89
|
+
app: int
|
|
90
|
+
status: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SalesOrderShipment(BaseModel):
|
|
94
|
+
date: str
|
|
95
|
+
products: List[SalesOrderShipmentProduct]
|
|
96
|
+
method: SalesOrderShipmentMethod
|
|
97
|
+
integration: Optional[SalesOrderShipmentIntegration] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SalesOrderResultItem(BaseModel):
|
|
101
|
+
codes: List[str]
|
|
102
|
+
contact_id: Optional[int] = Field(default=None, alias="contactId")
|
|
103
|
+
currency: str
|
|
104
|
+
date: str
|
|
105
|
+
delivery_method: str = Field(alias="deliveryMethod")
|
|
106
|
+
delivery_status: str = Field(alias="deliveryStatus")
|
|
107
|
+
id: str
|
|
108
|
+
integration_ids: List[str] = Field(alias="integrationIds")
|
|
109
|
+
integrations: List[SalesOrderIntegration]
|
|
110
|
+
invoice_integration_app: Optional[int] = Field(default=None, alias="invoiceIntegrationApp")
|
|
111
|
+
invoice_integration_id: Optional[str] = Field(default=None, alias="invoiceIntegrationId")
|
|
112
|
+
lines: List[SalesOrderLine]
|
|
113
|
+
payments: Optional[List[SalesOrderPayment]] = None
|
|
114
|
+
payment_status: str = Field(alias="paymentStatus")
|
|
115
|
+
payment_term: str = Field(alias="paymentTerm")
|
|
116
|
+
product_names: List[str] = Field(alias="productNames")
|
|
117
|
+
reserving_product_ids: Union[str, List[str]] = Field(alias="reservingProductIds")
|
|
118
|
+
sales_channel: int = Field(alias="salesChannel")
|
|
119
|
+
shipments: Optional[List[SalesOrderShipment]] = None
|
|
120
|
+
tracking_number: Optional[str] = Field(alias="trackingNumber")
|
|
121
|
+
skus: List[str]
|
|
122
|
+
status: str
|
|
123
|
+
tags: List[str]
|
|
124
|
+
warehouse: str
|
|
125
|
+
company_id: int = Field(alias="companyId")
|
|
126
|
+
shipping_cost: float = Field(alias="shippingCost")
|
|
127
|
+
contact_phone: Optional[str] = Field(default=None, alias="contactPhone")
|
|
128
|
+
brands: List[str]
|
|
129
|
+
courier: Optional[str] = None
|
|
130
|
+
order_id: int = Field(alias="orderId")
|
|
131
|
+
updated_at: str = Field(alias="updatedAt")
|
|
132
|
+
invoice_integration_created_at: Optional[str] = Field(default=None, alias="invoiceIntegrationCreatedAt")
|
|
133
|
+
invoice_integration_document_url: Optional[str] = Field(default=None, alias="invoiceIntegrationDocumentUrl")
|
|
134
|
+
has_document_url: bool = Field(alias="hasDocumentUrl")
|
|
135
|
+
integration_alternate_ids: Union[str, List[str]] = Field(alias="integrationAlternateIds")
|
|
136
|
+
cart_id: Optional[str] = Field(default=None, alias="cartId")
|
|
137
|
+
amount: float
|
|
138
|
+
has_any_shipments: bool = Field(alias="hasAnyShipments")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class SearchSalesOrder(BaseModel):
|
|
142
|
+
count: int
|
|
143
|
+
results: List[SalesOrderResultItem]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class SearchSalesOrderParams(BaseModel):
|
|
147
|
+
top: Optional[int]
|
|
148
|
+
skip: Optional[int]
|
|
149
|
+
filter: Optional[str] = Field(default=None, alias="$filter")
|
|
150
|
+
|
|
151
|
+
class Config:
|
|
152
|
+
validate_by_name = True
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"count": 0,
|
|
3
|
+
"results": [
|
|
4
|
+
{
|
|
5
|
+
"codes": [
|
|
6
|
+
"string"
|
|
7
|
+
],
|
|
8
|
+
"contactId": 0,
|
|
9
|
+
"currency": "Local",
|
|
10
|
+
"date": "string",
|
|
11
|
+
"deliveryMethod": "ToBeConfirmed",
|
|
12
|
+
"deliveryStatus": "ToBeConfirmed",
|
|
13
|
+
"id": "string",
|
|
14
|
+
"integrationIds": [
|
|
15
|
+
"string"
|
|
16
|
+
],
|
|
17
|
+
"integrations": [
|
|
18
|
+
{
|
|
19
|
+
"alternateId": "string",
|
|
20
|
+
"integrationId": 0,
|
|
21
|
+
"app": 0
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"invoiceIntegrationApp": 0,
|
|
25
|
+
"invoiceIntegrationId": "string",
|
|
26
|
+
"lines": [
|
|
27
|
+
{
|
|
28
|
+
"product": {
|
|
29
|
+
"id": 0,
|
|
30
|
+
"name": "string",
|
|
31
|
+
"code": "string",
|
|
32
|
+
"brand": "string"
|
|
33
|
+
},
|
|
34
|
+
"variation": {
|
|
35
|
+
"id": 0,
|
|
36
|
+
"attributes": [
|
|
37
|
+
{
|
|
38
|
+
"key": "string",
|
|
39
|
+
"value": "string"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"sku": "string",
|
|
43
|
+
"thumbnail": "string"
|
|
44
|
+
},
|
|
45
|
+
"quantity": 0,
|
|
46
|
+
"price": 0
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"payments": [
|
|
50
|
+
{
|
|
51
|
+
"date": "string",
|
|
52
|
+
"amount": 0,
|
|
53
|
+
"couponAmount": 0,
|
|
54
|
+
"status": "Pending",
|
|
55
|
+
"method": "Cash",
|
|
56
|
+
"integration": {
|
|
57
|
+
"integrationId": "string",
|
|
58
|
+
"app": 249
|
|
59
|
+
},
|
|
60
|
+
"transactionFee": 0,
|
|
61
|
+
"installments": 0,
|
|
62
|
+
"card": {
|
|
63
|
+
"paymentNetwork": "string",
|
|
64
|
+
"firstSixDigits": 0,
|
|
65
|
+
"lastFourDigits": 0,
|
|
66
|
+
"cardholderIdentificationNumber": "string",
|
|
67
|
+
"cardholderIdentificationType": "string",
|
|
68
|
+
"cardholderName": "string"
|
|
69
|
+
},
|
|
70
|
+
"notes": "string",
|
|
71
|
+
"hasCancelableStatus": true,
|
|
72
|
+
"id": 0
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"paymentStatus": "Pending",
|
|
76
|
+
"paymentTerm": "Advance",
|
|
77
|
+
"productNames": [
|
|
78
|
+
"string"
|
|
79
|
+
],
|
|
80
|
+
"reservingProductIds": "string",
|
|
81
|
+
"salesChannel": 0,
|
|
82
|
+
"shipments": [
|
|
83
|
+
{
|
|
84
|
+
"date": "string",
|
|
85
|
+
"products": [
|
|
86
|
+
{
|
|
87
|
+
"product": 0,
|
|
88
|
+
"variation": 0,
|
|
89
|
+
"quantity": 0
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"method": {
|
|
93
|
+
"trackingNumber": "string",
|
|
94
|
+
"trackingUrl": "string",
|
|
95
|
+
"courier": "string",
|
|
96
|
+
"mode": "string",
|
|
97
|
+
"cost": 0,
|
|
98
|
+
"type": "Ship",
|
|
99
|
+
"eta": "string",
|
|
100
|
+
"status": "PickingPending"
|
|
101
|
+
},
|
|
102
|
+
"integration": {
|
|
103
|
+
"id": 0,
|
|
104
|
+
"integrationId": "string",
|
|
105
|
+
"app": 0,
|
|
106
|
+
"status": "NotAvailable"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"trackingNumber": "string",
|
|
111
|
+
"skus": [
|
|
112
|
+
"string"
|
|
113
|
+
],
|
|
114
|
+
"status": "Pending",
|
|
115
|
+
"tags": [
|
|
116
|
+
"string"
|
|
117
|
+
],
|
|
118
|
+
"warehouse": "string",
|
|
119
|
+
"companyId": 0,
|
|
120
|
+
"shippingCost": 0,
|
|
121
|
+
"contactPhone": "string",
|
|
122
|
+
"brands": [
|
|
123
|
+
"string"
|
|
124
|
+
],
|
|
125
|
+
"courier": "string",
|
|
126
|
+
"orderId": 0,
|
|
127
|
+
"updatedAt": "string",
|
|
128
|
+
"invoiceIntegrationCreatedAt": "string",
|
|
129
|
+
"invoiceIntegrationDocumentUrl": "string",
|
|
130
|
+
"hasDocumentUrl": true,
|
|
131
|
+
"integrationAlternateIds": "string",
|
|
132
|
+
"cartId": "string",
|
|
133
|
+
"amount": 0,
|
|
134
|
+
"hasAnyShipments": true
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
from unittest.mock import patch, Mock
|
|
3
|
-
from producteca.
|
|
4
|
-
from producteca.
|
|
3
|
+
from producteca.sales_orders.sales_orders import SaleOrder
|
|
4
|
+
from producteca.client import ProductecaClient
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class TestSaleOrder(unittest.TestCase):
|
|
8
|
+
|
|
8
9
|
def setUp(self):
|
|
9
|
-
self.
|
|
10
|
+
self.client = ProductecaClient(token="test_client", api_key="test_secret")
|
|
10
11
|
self.sale_order_id = 123
|
|
11
12
|
self.mock_response = {
|
|
12
13
|
"id": self.sale_order_id,
|
|
13
14
|
"contact": {"id": 1, "name": "Test Contact"},
|
|
14
|
-
"lines": []
|
|
15
|
+
"lines": [],
|
|
16
|
+
"invoiceIntegration": {
|
|
17
|
+
'id': 1,
|
|
18
|
+
'integrationId': 'test-integration',
|
|
19
|
+
'app': 1,
|
|
20
|
+
'createdAt': '2023-01-01',
|
|
21
|
+
'decreaseStock': True
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
@patch('requests.get')
|
|
@@ -21,7 +29,7 @@ class TestSaleOrder(unittest.TestCase):
|
|
|
21
29
|
json=lambda: self.mock_response
|
|
22
30
|
)
|
|
23
31
|
|
|
24
|
-
sale_order =
|
|
32
|
+
sale_order = self.client.SalesOrder.get(self.sale_order_id)
|
|
25
33
|
self.assertEqual(sale_order.id, self.sale_order_id)
|
|
26
34
|
mock_get.assert_called_once()
|
|
27
35
|
|
|
@@ -33,20 +41,33 @@ class TestSaleOrder(unittest.TestCase):
|
|
|
33
41
|
json=lambda: mock_labels
|
|
34
42
|
)
|
|
35
43
|
|
|
36
|
-
labels =
|
|
44
|
+
labels = self.client.SalesOrder(id=1234, invoiceIntegration={
|
|
45
|
+
'id': 1,
|
|
46
|
+
'integrationId': 'test-integration',
|
|
47
|
+
'app': 1,
|
|
48
|
+
'createdAt': '2023-01-01',
|
|
49
|
+
'decreaseStock': True,
|
|
50
|
+
"documentUrl": "https://aallala.copm",
|
|
51
|
+
"xmlUrl": "https://aallala.copm",
|
|
52
|
+
}).get_shipping_labels()
|
|
37
53
|
self.assertEqual(labels, mock_labels)
|
|
38
54
|
mock_get.assert_called_once()
|
|
39
55
|
|
|
40
56
|
@patch('requests.post')
|
|
41
57
|
def test_close_sale_order(self, mock_post):
|
|
42
58
|
mock_post.return_value = Mock(
|
|
43
|
-
status_code=200
|
|
44
|
-
json=lambda: {"status": "closed"}
|
|
59
|
+
status_code=200
|
|
45
60
|
)
|
|
46
61
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
self.client.SalesOrder(id=1234, invoiceIntegration={
|
|
63
|
+
'id': 1,
|
|
64
|
+
'integrationId': 'test-integration',
|
|
65
|
+
'app': 1,
|
|
66
|
+
'createdAt': '2023-01-01',
|
|
67
|
+
'decreaseStock': True,
|
|
68
|
+
"documentUrl": "https://aallala.copm",
|
|
69
|
+
"xmlUrl": "https://aallala.copm",
|
|
70
|
+
}).close()
|
|
50
71
|
mock_post.assert_called_once()
|
|
51
72
|
|
|
52
73
|
@patch('requests.post')
|
|
@@ -56,9 +77,15 @@ class TestSaleOrder(unittest.TestCase):
|
|
|
56
77
|
json=lambda: {"status": "cancelled"}
|
|
57
78
|
)
|
|
58
79
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
self.client.SalesOrder(id=1234, invoiceIntegration={
|
|
81
|
+
'id': 1,
|
|
82
|
+
'integrationId': 'test-integration',
|
|
83
|
+
'app': 1,
|
|
84
|
+
'createdAt': '2023-01-01',
|
|
85
|
+
'decreaseStock': True,
|
|
86
|
+
"documentUrl": "https://aallala.copm",
|
|
87
|
+
"xmlUrl": "https://aallala.copm",
|
|
88
|
+
}).cancel()
|
|
62
89
|
mock_post.assert_called_once()
|
|
63
90
|
|
|
64
91
|
@patch('requests.post')
|
|
@@ -69,30 +96,32 @@ class TestSaleOrder(unittest.TestCase):
|
|
|
69
96
|
json=lambda: self.mock_response
|
|
70
97
|
)
|
|
71
98
|
|
|
72
|
-
|
|
73
|
-
self.assertEqual(status_code, 200)
|
|
99
|
+
response = self.client.SalesOrder(**sale_order.model_dump(by_alias=True)).synchronize()
|
|
74
100
|
self.assertEqual(response.id, self.sale_order_id)
|
|
75
101
|
mock_post.assert_called_once()
|
|
76
102
|
|
|
77
103
|
@patch('requests.put')
|
|
78
104
|
def test_invoice_integration(self, mock_put):
|
|
79
105
|
invoice_data = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
'id': 1,
|
|
107
|
+
'integrationId': 'test-integration',
|
|
108
|
+
'app': 1,
|
|
109
|
+
'createdAt': '2023-01-01',
|
|
110
|
+
'decreaseStock': True,
|
|
111
|
+
"documentUrl": "https://aallala.copm",
|
|
112
|
+
"xmlUrl": "https://aallala.copm",
|
|
113
|
+
}
|
|
86
114
|
|
|
87
115
|
mock_put.return_value = Mock(
|
|
88
116
|
status_code=200,
|
|
89
|
-
json=lambda:
|
|
117
|
+
json=lambda: invoice_data,
|
|
118
|
+
ok=True
|
|
90
119
|
)
|
|
91
120
|
|
|
92
|
-
|
|
93
|
-
self.
|
|
94
|
-
self.assertEqual(response, {})
|
|
121
|
+
response = self.client.SalesOrder(id=self.sale_order_id, invoiceIntegration=invoice_data).invoice_integration()
|
|
122
|
+
self.assertTrue(response)
|
|
95
123
|
mock_put.assert_called_once()
|
|
96
124
|
|
|
125
|
+
|
|
97
126
|
if __name__ == '__main__':
|
|
98
127
|
unittest.main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import json
|
|
3
|
+
from unittest.mock import patch, Mock
|
|
4
|
+
from producteca.sales_orders.search_sale_orders import SearchSalesOrderParams, SalesOrderResultItem
|
|
5
|
+
from producteca.client import ProductecaClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestSearchSalesOrder(unittest.TestCase):
|
|
9
|
+
|
|
10
|
+
def setUp(self):
|
|
11
|
+
self.client = ProductecaClient(token="test_client_id", api_key="test_client_secret")
|
|
12
|
+
self.params = SearchSalesOrderParams(
|
|
13
|
+
top=10,
|
|
14
|
+
skip=0,
|
|
15
|
+
filter="status eq 'confirmed'"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
@patch('requests.get')
|
|
19
|
+
def test_search_saleorder_success(self, mock_get):
|
|
20
|
+
# Mock successful response
|
|
21
|
+
mock_response = Mock()
|
|
22
|
+
with open('producteca/sales_orders/tests/search.json', 'r') as f:
|
|
23
|
+
results = json.loads(f.read())
|
|
24
|
+
mock_response.json.return_value = results
|
|
25
|
+
mock_response.status_code = 200
|
|
26
|
+
mock_get.return_value = mock_response
|
|
27
|
+
|
|
28
|
+
response = self.client.SalesOrder.search(self.params)
|
|
29
|
+
|
|
30
|
+
self.assertEqual(response.count, 0)
|
|
31
|
+
self.assertEqual(len(response.results), 1)
|
|
32
|
+
self.assertEqual(response.results[0].id, "string")
|
|
33
|
+
self.assertIsInstance(response.results[0], SalesOrderResultItem)
|
|
34
|
+
|
|
35
|
+
@patch('requests.get')
|
|
36
|
+
def test_search_saleorder_error(self, mock_get):
|
|
37
|
+
mock_response = Mock()
|
|
38
|
+
mock_response.json.return_value = {"error": "Invalid request"}
|
|
39
|
+
mock_response.status_code = 400
|
|
40
|
+
mock_get.return_value = mock_response
|
|
41
|
+
with self.assertRaises(Exception):
|
|
42
|
+
self.client.SalesOrder.search(self.params)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == '__main__':
|
|
46
|
+
unittest.main()
|
producteca/shipments/shipment.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from typing import List, Optional
|
|
2
2
|
from pydantic import BaseModel
|
|
3
|
-
import requests
|
|
4
|
-
from ..config.config import ConfigProducteca
|
|
5
3
|
|
|
6
4
|
|
|
7
5
|
class ShipmentProduct(BaseModel):
|
|
@@ -33,15 +31,3 @@ class Shipment(BaseModel):
|
|
|
33
31
|
products: Optional[List[ShipmentProduct]] = None
|
|
34
32
|
method: Optional[ShipmentMethod] = None
|
|
35
33
|
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()
|
|
@@ -1,57 +1,62 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
from unittest.mock import patch, MagicMock
|
|
3
|
-
from producteca.shipments.shipment import Shipment, ShipmentProduct, ShipmentMethod, ShipmentIntegration
|
|
3
|
+
from producteca.shipments.shipment import Shipment, ShipmentProduct, ShipmentMethod, ShipmentIntegration
|
|
4
|
+
from producteca.client import ProductecaClient
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class TestShipment(unittest.TestCase):
|
|
7
|
-
|
|
8
|
+
def setUp(self):
|
|
9
|
+
self.client = ProductecaClient(token="test_id", api_key="test_secret")
|
|
10
|
+
|
|
8
11
|
@patch('requests.post')
|
|
9
12
|
def test_create_shipment(self, mock_post):
|
|
10
13
|
# Arrange
|
|
11
|
-
config = ConfigProducteca(token="test_token", api_key="as")
|
|
12
|
-
sale_order_id = 123
|
|
13
14
|
products = [ShipmentProduct(product=1, variation=2, quantity=3)]
|
|
14
15
|
method = ShipmentMethod(trackingNumber="TN123", trackingUrl="http://track.url", courier="DHL", mode="air", cost=10.5, type="express", eta=5, status="shipped")
|
|
15
16
|
integration = ShipmentIntegration(id=1, integrationId="int123", app=10, status="active")
|
|
16
|
-
payload = Shipment(date="2023-01-01", products=products, method=method, integration=integration)
|
|
17
|
+
payload = Shipment(date="2023-01-01", products=products, method=method, integration=integration).model_dump(by_alias=True)
|
|
17
18
|
|
|
18
19
|
mock_response = MagicMock()
|
|
19
20
|
mock_response.status_code = 201
|
|
20
|
-
mock_response.json.return_value =
|
|
21
|
+
mock_response.json.return_value = payload
|
|
21
22
|
mock_post.return_value = mock_response
|
|
22
|
-
|
|
23
23
|
# Act
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
shipment = self.client.SalesOrder(id=1234, invoiceIntegration={
|
|
25
|
+
'id': 1,
|
|
26
|
+
'integrationId': 'test-integration',
|
|
27
|
+
'app': 1,
|
|
28
|
+
'createdAt': '2023-01-01',
|
|
29
|
+
'decreaseStock': True
|
|
30
|
+
}).add_shipment(payload)
|
|
31
|
+
|
|
32
|
+
self.assertIsInstance(shipment, Shipment)
|
|
29
33
|
mock_post.assert_called_once()
|
|
30
34
|
|
|
31
35
|
@patch('requests.put')
|
|
32
36
|
def test_update_shipment(self, mock_put):
|
|
33
37
|
# Arrange
|
|
34
|
-
config = ConfigProducteca(token="test_token", api_key="as")
|
|
35
|
-
sale_order_id = 123
|
|
36
38
|
shipment_id = 'abc'
|
|
37
39
|
products = [ShipmentProduct(product=4, quantity=7)]
|
|
38
40
|
method = ShipmentMethod(courier="FedEx", cost=15.0)
|
|
39
41
|
integration = ShipmentIntegration(status="pending")
|
|
40
|
-
payload = Shipment(date="2023-02-02", products=products, method=method, integration=integration)
|
|
42
|
+
payload = Shipment(date="2023-02-02", products=products, method=method, integration=integration).model_dump(by_alias=True)
|
|
41
43
|
|
|
42
44
|
mock_response = MagicMock()
|
|
43
45
|
mock_response.status_code = 200
|
|
44
|
-
mock_response.json.return_value =
|
|
46
|
+
mock_response.json.return_value = payload
|
|
45
47
|
mock_put.return_value = mock_response
|
|
46
|
-
|
|
47
48
|
# Act
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
shipment = self.client.SalesOrder(id=1234, invoiceIntegration={
|
|
50
|
+
'id': 1,
|
|
51
|
+
'integrationId': 'test-integration',
|
|
52
|
+
'app': 1,
|
|
53
|
+
'createdAt': '2023-01-01',
|
|
54
|
+
'decreaseStock': True
|
|
55
|
+
}).update_shipment(shipment_id, payload)
|
|
56
|
+
|
|
57
|
+
self.assertIsInstance(shipment, Shipment)
|
|
53
58
|
mock_put.assert_called_once()
|
|
54
59
|
|
|
55
60
|
|
|
56
61
|
if __name__ == '__main__':
|
|
57
|
-
unittest.main()
|
|
62
|
+
unittest.main()
|
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)
|