spyreapi 0.0.7__tar.gz → 0.0.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {spyreapi-0.0.7/src/spyreapi.egg-info → spyreapi-0.0.8}/PKG-INFO +1 -1
- {spyreapi-0.0.7 → spyreapi-0.0.8}/pyproject.toml +1 -1
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/inventory_models.py +1 -7
- spyreapi-0.0.8/src/spyre/Models/purchasing_models.py +77 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/shared_models.py +8 -1
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/__init__.py +11 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/client.py +6 -3
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/inventory.py +5 -5
- spyreapi-0.0.8/src/spyre/purchasing.py +363 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/spire.py +5 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8/src/spyreapi.egg-info}/PKG-INFO +1 -1
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyreapi.egg-info/SOURCES.txt +2 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/LICENSE +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/MANIFEST.in +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/README.md +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/setup.cfg +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Exceptions.py +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/__init__.py +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/customers_models.py +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/sales_models.py +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/crm.py +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/customers.py +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/sales.py +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/utils.py +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyreapi.egg-info/dependency_links.txt +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyreapi.egg-info/requires.txt +0 -0
- {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyreapi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spyreapi
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
4
4
|
Summary: A robust and extensible Python client for interacting with the [Spire Business Software API](https://developer.spiresystems.com/reference). This client provides an object-oriented interface to get, create, update, delete, query, filter, sort, and manage various Spire modules such as Sales Orders, Invoices, Inventory Items, and more.
|
|
5
5
|
Author-email: Sanjid Sharaf <sanjidsharaf1@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import List, Dict, Optional, Union
|
|
2
2
|
from pydantic import BaseModel
|
|
3
|
-
|
|
3
|
+
from .shared_models import Vendor
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class UPC(BaseModel):
|
|
@@ -16,12 +16,6 @@ class UPC(BaseModel):
|
|
|
16
16
|
modifiedBy: Optional[str] = None
|
|
17
17
|
links: Optional[Dict[str, str]] = None
|
|
18
18
|
|
|
19
|
-
class Vendor(BaseModel):
|
|
20
|
-
id: Optional[int] = None
|
|
21
|
-
vendorNo: Optional[str] = None
|
|
22
|
-
name: Optional[str] = None
|
|
23
|
-
|
|
24
|
-
|
|
25
19
|
class UnitOfMeasure(BaseModel):
|
|
26
20
|
id: Optional[int] = None
|
|
27
21
|
code: Optional[str] = None
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import List, Optional, Dict, Any, Union
|
|
2
|
+
from pydantic import BaseModel, model_validator
|
|
3
|
+
from .shared_models import Vendor, Address, Currency, Contact
|
|
4
|
+
|
|
5
|
+
class InventoryRef(BaseModel):
|
|
6
|
+
id: Optional[int] = None
|
|
7
|
+
whse: Optional[str] = None
|
|
8
|
+
partNo: Optional[str] = None
|
|
9
|
+
description: Optional[str] = None
|
|
10
|
+
|
|
11
|
+
class PurchaseOrderItem(BaseModel):
|
|
12
|
+
id: Optional[int] = None
|
|
13
|
+
whse: Optional[str] = None
|
|
14
|
+
partNo: Optional[str] = None
|
|
15
|
+
sequence: Optional[int] = None
|
|
16
|
+
inventory: Optional[InventoryRef] = None
|
|
17
|
+
serials: Optional[Any] = None
|
|
18
|
+
description: Optional[str] = None
|
|
19
|
+
orderQty: Optional[str] = None
|
|
20
|
+
receiveQty: Optional[str] = None
|
|
21
|
+
receivedQty: Optional[str] = None
|
|
22
|
+
unitPrice: Optional[str] = None
|
|
23
|
+
freight: Optional[str] = None
|
|
24
|
+
purchaseMeasure: Optional[str] = None
|
|
25
|
+
taxFlags: Optional[List[bool]] = None
|
|
26
|
+
udf: Optional[Dict[str,Any]] = None
|
|
27
|
+
requiredDate: Optional[str] = None
|
|
28
|
+
referenceNo: Optional[str] = None
|
|
29
|
+
comment: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
class PurchaseOrder(BaseModel):
|
|
32
|
+
id: Optional[int] = None
|
|
33
|
+
number: Optional[str] = None
|
|
34
|
+
vendor: Optional[Vendor] = None
|
|
35
|
+
currency: Optional[Union[Currency, Dict[str, Any]]] = None
|
|
36
|
+
status: Optional[str] = None
|
|
37
|
+
Date: Optional[str] = None
|
|
38
|
+
requiredDate: Optional[str] = None
|
|
39
|
+
address: Optional[Address] = None
|
|
40
|
+
shippingAddress: Optional[Address] = None
|
|
41
|
+
vendorPO: Optional[str] = None
|
|
42
|
+
referenceNo: Optional[str] = None
|
|
43
|
+
fob: Optional[str] = None
|
|
44
|
+
incoterms: Optional[str] = None
|
|
45
|
+
incotermsPlace: Optional[str] = None
|
|
46
|
+
subtotal: Optional[str] = None
|
|
47
|
+
total: Optional[str] = None
|
|
48
|
+
deposit: Optional[str] = None
|
|
49
|
+
items: Optional[List[PurchaseOrderItem]] = None
|
|
50
|
+
udf: Optional[Dict[str,Any]] = None
|
|
51
|
+
createdBy: Optional[str] = None
|
|
52
|
+
modifiedBy: Optional[str] = None
|
|
53
|
+
created: Optional[str] = None
|
|
54
|
+
modified: Optional[str] = None
|
|
55
|
+
links: Optional[Dict[str,str]] = None
|
|
56
|
+
|
|
57
|
+
@model_validator(mode="before")
|
|
58
|
+
@classmethod
|
|
59
|
+
def clean_problematic_fields(cls, data: dict) -> dict:
|
|
60
|
+
if data.get("currency") == "":
|
|
61
|
+
data["currency"] = None
|
|
62
|
+
|
|
63
|
+
contact = data.get("contact")
|
|
64
|
+
if contact:
|
|
65
|
+
for field in ("phone", "fax"):
|
|
66
|
+
phone_data = contact.get(field)
|
|
67
|
+
if isinstance(phone_data, dict) and phone_data.get("number") is None:
|
|
68
|
+
phone_data["number"] = ""
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
@@ -122,4 +122,11 @@ class Note(BaseModel):
|
|
|
122
122
|
createdBy: Optional[str] = None
|
|
123
123
|
modified: Optional[str] = None
|
|
124
124
|
modifiedBy: Optional[str] = None
|
|
125
|
-
links: Optional[Dict[str, str]] = None
|
|
125
|
+
links: Optional[Dict[str, str]] = None
|
|
126
|
+
|
|
127
|
+
class Vendor(BaseModel):
|
|
128
|
+
id: Optional[int] = None
|
|
129
|
+
vendorNo: Optional[str] = None
|
|
130
|
+
code: Optional[str] = None
|
|
131
|
+
name: Optional[str] = None
|
|
132
|
+
|
|
@@ -5,7 +5,10 @@ from .sales import OrdersClient, InvoiceClient, salesOrder, invoice
|
|
|
5
5
|
from .customers import CustomerClient, customer
|
|
6
6
|
from .Models.sales_models import SalesOrder, SalesOrderItem
|
|
7
7
|
from .Models.inventory_models import InventoryItem, Vendor, UnitOfMeasure, Pricing, UPC
|
|
8
|
+
from .Models.shared_models import Currency, Address
|
|
8
9
|
from .Exceptions import CreateRequestError
|
|
10
|
+
from .Models.purchasing_models import PurchaseOrderItem, PurchaseOrder, InventoryRef
|
|
11
|
+
from .purchasing import purchaseOrder, PurchasingClient, PurchasingHistoryClient
|
|
9
12
|
|
|
10
13
|
__all__ = [
|
|
11
14
|
"SpireClient",
|
|
@@ -27,4 +30,12 @@ __all__ = [
|
|
|
27
30
|
"CustomerClient",
|
|
28
31
|
"customer",
|
|
29
32
|
"CreateRequestError"
|
|
33
|
+
"PurchaseOrder",
|
|
34
|
+
"purchaseOrder"
|
|
35
|
+
"PurchaseOrderItem"
|
|
36
|
+
"PurchasingClient",
|
|
37
|
+
"PurchasingHistoryClient",
|
|
38
|
+
"InventoryRef",
|
|
39
|
+
"Currency",
|
|
40
|
+
"Address"
|
|
30
41
|
]
|
|
@@ -9,7 +9,7 @@ T = TypeVar('T', bound=BaseModel)
|
|
|
9
9
|
class SpireClient():
|
|
10
10
|
"""A lightweight to interact with the Spire API using requests sessions for connection reuse and authenticated calls."""
|
|
11
11
|
|
|
12
|
-
def __init__(self, host, company, username, password,):
|
|
12
|
+
def __init__(self, host, company, username, password, secure = True):
|
|
13
13
|
"""
|
|
14
14
|
Initialize a SpireClient instance.
|
|
15
15
|
|
|
@@ -18,6 +18,7 @@ class SpireClient():
|
|
|
18
18
|
company (str): Spire company.
|
|
19
19
|
username (str): Spire user username.
|
|
20
20
|
password (str): Spire user password.
|
|
21
|
+
secure (bool): HTTPS or HTTP connection
|
|
21
22
|
"""
|
|
22
23
|
self.session = requests.Session()
|
|
23
24
|
self.session.auth = (username, password)
|
|
@@ -25,8 +26,10 @@ class SpireClient():
|
|
|
25
26
|
"accept": "application/json",
|
|
26
27
|
"content-type": "application/json"
|
|
27
28
|
})
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
if secure:
|
|
30
|
+
self.base_url = f"https://{host}/api/v2/companies/{company}"
|
|
31
|
+
else:
|
|
32
|
+
self.base_url = f"http://{host}/api/v2/companies/{company}"
|
|
30
33
|
try:
|
|
31
34
|
response = self.session.get(self.base_url)
|
|
32
35
|
if response.text == 'No such company intertes':
|
|
@@ -260,7 +260,7 @@ class ItemsClient():
|
|
|
260
260
|
Args:
|
|
261
261
|
item_id (int): The ID of the inventory item.
|
|
262
262
|
uom_id (int): The ID of the UOM to update.
|
|
263
|
-
|
|
263
|
+
uom_record (uom): A `uom` instance with updated field values.
|
|
264
264
|
|
|
265
265
|
Returns:
|
|
266
266
|
uom: The updated `uom` instance returned from the API.
|
|
@@ -364,7 +364,7 @@ class ItemsClient():
|
|
|
364
364
|
Args:
|
|
365
365
|
item_id (int): The ID of the inventory item.
|
|
366
366
|
upc_id (int): The ID of the upc to update.
|
|
367
|
-
|
|
367
|
+
upc_record (upc): A `upc` instance with updated field values.
|
|
368
368
|
|
|
369
369
|
Returns:
|
|
370
370
|
upc: The updated `upc` instance returned from the API.
|
|
@@ -452,7 +452,7 @@ class item(APIResource[InventoryItem]):
|
|
|
452
452
|
and returns the created UOM.
|
|
453
453
|
|
|
454
454
|
Args:
|
|
455
|
-
|
|
455
|
+
uom_record (UnitOfMeasure): The UnitOfMeasure Pydantic model to be created.
|
|
456
456
|
|
|
457
457
|
Returns:
|
|
458
458
|
uom (uom): The newly created Unit of measure as a `uom` instance.
|
|
@@ -480,7 +480,7 @@ class item(APIResource[InventoryItem]):
|
|
|
480
480
|
and returns the created UPC.
|
|
481
481
|
|
|
482
482
|
Args:
|
|
483
|
-
|
|
483
|
+
upc_record (UPC): The UPC Pydantic model to be created.
|
|
484
484
|
|
|
485
485
|
Returns:
|
|
486
486
|
upc (upc): The newly created UPC as a `upc` instance.
|
|
@@ -619,7 +619,7 @@ class UpcClient():
|
|
|
619
619
|
|
|
620
620
|
Args:
|
|
621
621
|
upc_id (int): The ID of the upc to update.
|
|
622
|
-
|
|
622
|
+
upc_record (upc): A `upc` instance with updated field values.
|
|
623
623
|
|
|
624
624
|
Returns:
|
|
625
625
|
upc: The updated `upc` instance returned from the API.
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
from .client import APIResource, SpireClient
|
|
2
|
+
from requests.exceptions import HTTPError, RequestException
|
|
3
|
+
from .Exceptions import CreateRequestError
|
|
4
|
+
from typing import Any, Optional, List, Dict
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
from .utils import *
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from .Models.purchasing_models import PurchaseOrder
|
|
10
|
+
|
|
11
|
+
class PurchasingClient:
|
|
12
|
+
|
|
13
|
+
def __init__(self, client: SpireClient):
|
|
14
|
+
self.client = client
|
|
15
|
+
self.endpoint = "purchasing/orders"
|
|
16
|
+
|
|
17
|
+
def get_purchase_order(self, id: int = None, PO_number: str = None) -> 'purchaseOrder':
|
|
18
|
+
"""
|
|
19
|
+
Retrieve a purchase order by its ID or PO number.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
id (int, optional): The ID of the purchase order to retrieve.
|
|
23
|
+
PO_number (str, optional): The purchase order number of the purchase order to retrieve.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
purchaseOrder: A `purchaseOrder` wrapper instance containing the retrieved data.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If neither id nor PO_number is provided, or if no matching order is found.
|
|
30
|
+
"""
|
|
31
|
+
if id is not None:
|
|
32
|
+
response = self.client._get(f"/{self.endpoint}/{str(id)}")
|
|
33
|
+
return purchaseOrder.from_json(response, self.client)
|
|
34
|
+
elif PO_number is not None:
|
|
35
|
+
orders = self.query_purchase_order(query=PO_number)
|
|
36
|
+
for order in orders:
|
|
37
|
+
if getattr(order, "number", None) == PO_number:
|
|
38
|
+
return self.get_purchase_order(order.id)
|
|
39
|
+
raise ValueError(f"No purchase order found for purchase order {PO_number}")
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError("Either 'id' or 'PO_number' must be provided.")
|
|
42
|
+
|
|
43
|
+
def create_purchase_order(self, purchase_order: 'PurchaseOrder') -> 'purchaseOrder':
|
|
44
|
+
"""
|
|
45
|
+
Create a new purchase order.
|
|
46
|
+
|
|
47
|
+
Sends a POST request to the purchase order endpoint.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
purchase_order (dict): A PurchaseOrder instance containing the purchase order details.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
purchaseOrder: The create PurchaseOrder instance.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
CreateRequestError: If the creation fails or response is invalid.
|
|
57
|
+
"""
|
|
58
|
+
if hasattr(purchase_order, "model_dump_json"): # Pydantic v2
|
|
59
|
+
payload = json.loads(
|
|
60
|
+
purchase_order.model_dump_json(
|
|
61
|
+
exclude_unset=True,
|
|
62
|
+
exclude_none=True,
|
|
63
|
+
by_alias=True, # keep camelCase if you use aliases
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
else: # Pydantic v1 fallback
|
|
67
|
+
payload = json.loads(
|
|
68
|
+
purchase_order.json(
|
|
69
|
+
exclude_unset=True,
|
|
70
|
+
exclude_none=True,
|
|
71
|
+
by_alias=True,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
response = self.client._post(f"/{self.endpoint}", json=payload)
|
|
75
|
+
if response.get('status_code') == 201:
|
|
76
|
+
location = response.get('headers').get('location')
|
|
77
|
+
parsed_url = urlparse(location)
|
|
78
|
+
path_segments = parsed_url.path.rstrip("/").split("/")
|
|
79
|
+
id = path_segments[-1]
|
|
80
|
+
return self.get_purchase_order(id)
|
|
81
|
+
else:
|
|
82
|
+
error_message = response.get('content')
|
|
83
|
+
raise CreateRequestError(self.endpoint, status_code=response.get('status_code'), error_message=error_message)
|
|
84
|
+
|
|
85
|
+
def update_purchase_order(self, id: int, purchase_order: 'purchaseOrder') -> 'purchaseOrder':
|
|
86
|
+
"""
|
|
87
|
+
Update an existing purchase order by ID.
|
|
88
|
+
|
|
89
|
+
Sends a PUT request to the purchase order endpoint with the provided sales order data
|
|
90
|
+
to update the existing record. Returns a wrapped `purchaseOrder` object containing
|
|
91
|
+
the updated information.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
id (int): The ID of the purchase order to update.
|
|
95
|
+
purchase_order (PurchaseOrder): A PurchaseOrder instance with the purchase order details.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
purchaseOrder: An instance of the purchaseOrder wrapper class initialized with the
|
|
99
|
+
updated data and client session.
|
|
100
|
+
"""
|
|
101
|
+
if purchase_order.status == "I":
|
|
102
|
+
raise ValueError(f"Cannot update an issued purchase order for {purchase_order.number}")
|
|
103
|
+
else:
|
|
104
|
+
response = self.client._put(f"/{self.endpoint}/{str(id)}", json=purchase_order.model_dump(exclude_none=True, exclude_unset=True))
|
|
105
|
+
return purchaseOrder.from_json(response, self.client)
|
|
106
|
+
|
|
107
|
+
def delete_purchase_order(self, id: int) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Delete a purchase order by its ID.
|
|
110
|
+
|
|
111
|
+
Sends a DELETE request to the purchase order endpoint to remove the specified
|
|
112
|
+
purchase order from the system.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
id (int): The ID of the purchase order to delete.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
bool: True if the sales order was successfully deleted, False otherwise.
|
|
119
|
+
"""
|
|
120
|
+
order = self.get_purchase_order(id)
|
|
121
|
+
if order.model.status in ("I", "R"):
|
|
122
|
+
raise ValueError(f"Cannot delete an issued or received purchase order for {order.number}")
|
|
123
|
+
else:
|
|
124
|
+
return self.client._delete(f"/{self.endpoint}/{str(id)}")
|
|
125
|
+
|
|
126
|
+
def query_purchase_order(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
query: Optional[str] = None,
|
|
130
|
+
sort: Optional[Dict[str,str]] = None,
|
|
131
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
132
|
+
all: bool = False,
|
|
133
|
+
limit: int = 1000,
|
|
134
|
+
start: int = 0,
|
|
135
|
+
**extra_params
|
|
136
|
+
) -> List["purchaseOrder"]:
|
|
137
|
+
"""
|
|
138
|
+
Query purchase orders with optional full-text search, filtering, multi-field sorting, and pagination.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
query (str, optional): Full-text search string.
|
|
142
|
+
sort (dict, optional): Dictionary of sorting rules (e.g., {"date": "desc", "number": "asc"}).
|
|
143
|
+
filter (dict, optional): Dictionary of filters to apply (will be JSON-encoded and URL-safe).
|
|
144
|
+
all (bool, optional): If True, retrieves all pages of results.
|
|
145
|
+
limit (int, optional): Number of results per page (max 1000).
|
|
146
|
+
start (int, optional): Starting offset for pagination.
|
|
147
|
+
**extra_params (Any): Any additional parameters to include in the query.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
List[purchaseOrder]: List of wrapped purchase order resources.
|
|
151
|
+
"""
|
|
152
|
+
return self.client._query(
|
|
153
|
+
endpoint=self.endpoint,
|
|
154
|
+
resource_cls=purchaseOrder,
|
|
155
|
+
query=query,
|
|
156
|
+
sort=sort,
|
|
157
|
+
filter=filter,
|
|
158
|
+
all=all,
|
|
159
|
+
limit=limit,
|
|
160
|
+
start=start,
|
|
161
|
+
**extra_params
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def issue_purchase_order(self, id:int) -> 'purchaseOrder':
|
|
165
|
+
"""
|
|
166
|
+
Issue a purchase order by its ID.
|
|
167
|
+
|
|
168
|
+
Sends a request to Spire to change the purchase order's status to
|
|
169
|
+
“Issued”. Issuing an order typically means it has been confirmed
|
|
170
|
+
and is ready to be sent to the vendor.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
id (int): The ID of the purchaseOrder to issue.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
purchaseOrder: The updated purchase order object with the status set to "I" (Issued).
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
CreateRequestError: If the request fails or the API returns an error status.
|
|
180
|
+
"""
|
|
181
|
+
response = self.client._post(f"/{self.endpoint}/{str(id)}/issue")
|
|
182
|
+
if response.get("status_code") != 200:
|
|
183
|
+
raise CreateRequestError(f"Failed to issue purchase order: {id}: {response.text}")
|
|
184
|
+
return purchaseOrder.from_json(response, self.client)
|
|
185
|
+
|
|
186
|
+
def receive_purchase_order(self, id: int, receiveAll: bool = None) -> 'purchaseOrder':
|
|
187
|
+
"""
|
|
188
|
+
Receive a purchase order by its ID.
|
|
189
|
+
|
|
190
|
+
This updates the order in Spire to reflect that items have been received.
|
|
191
|
+
Typically, this should only be called on purchase orders with status "Issued".
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
id (int): The ID of the purchaseOrder to receive.
|
|
195
|
+
receiveAll (bool, optional): An optional boolean to recieve all quantites on the purchase order.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
purchaseOrder: The updated purchase order object with the status set to "R" (Received).
|
|
199
|
+
|
|
200
|
+
Raises: CreateRequestError: If the request fails or the API returns an error status.
|
|
201
|
+
"""
|
|
202
|
+
if receiveAll:
|
|
203
|
+
order = self.get_purchase_order(id)
|
|
204
|
+
for item in order.model.items:
|
|
205
|
+
item.receiveQty = item.orderQty
|
|
206
|
+
order.update()
|
|
207
|
+
response = self.client._post(f"/{self.endpoint}/{str(id)}/receive")
|
|
208
|
+
if response.get("status_code") != 200:
|
|
209
|
+
raise CreateRequestError(f"Failed to receive purchase order: {id}: {response.text}")
|
|
210
|
+
return purchaseOrder.from_json(response, self.client)
|
|
211
|
+
|
|
212
|
+
class PurchasingHistoryClient:
|
|
213
|
+
|
|
214
|
+
def __init__(self, client: SpireClient):
|
|
215
|
+
self.client = client
|
|
216
|
+
self.endpoint = "purchasing/history"
|
|
217
|
+
|
|
218
|
+
def get_purchase_history_order(self, id: int = None, PO_number: str = None) -> 'purchaseOrder':
|
|
219
|
+
"""
|
|
220
|
+
Retrieve an archived purchase order by its ID or PO number.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
id (int, optional): The ID of the purchase order to retrieve.
|
|
224
|
+
PO_number (str, optional): The purchase order number of the purchase order to retrieve.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
purchaseOrder: A `purchaseOrder` wrapper instance containing the retrieved data.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
ValueError: If neither id nor PO_number is provieded, or if no matching purchase order is found.
|
|
231
|
+
"""
|
|
232
|
+
if id is not None:
|
|
233
|
+
response = self.client._get(f"/{self.endpoint}/{str(id)}")
|
|
234
|
+
return purchaseOrder.from_json(response, self.client)
|
|
235
|
+
elif PO_number is not None:
|
|
236
|
+
orders = self.query_purchase_history_order(query=PO_number)
|
|
237
|
+
for order in orders:
|
|
238
|
+
if getattr(order, "number", None) == PO_number:
|
|
239
|
+
return self.get_purchase_history_order(order.id)
|
|
240
|
+
raise ValueError(f"No purchase order found for purchase order {PO_number}")
|
|
241
|
+
else:
|
|
242
|
+
raise ValueError("Either 'id' or 'PO_number' must be provided.")
|
|
243
|
+
|
|
244
|
+
def query_purchase_history_order(
|
|
245
|
+
self,
|
|
246
|
+
*,
|
|
247
|
+
query: Optional[str] = None,
|
|
248
|
+
sort: Optional[Dict[str,str]] = None,
|
|
249
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
250
|
+
all: bool = False,
|
|
251
|
+
limit: int = 1000,
|
|
252
|
+
start: int = 0,
|
|
253
|
+
**extra_params
|
|
254
|
+
) -> List["purchaseOrder"]:
|
|
255
|
+
"""
|
|
256
|
+
Query archived purchase orders with optional full-text search, filtering, multi-field sorting, and pagination.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
query (str, optional): Full-text search string.
|
|
260
|
+
sort (dict, optional): Dictionary of sorting rules (e.g., {"date": "desc", "number": "asc"}).
|
|
261
|
+
filter (dict, optional): Dictionary of filters to apply (will be JSON-encoded and URL-safe).
|
|
262
|
+
all (bool, optional): If True, retrieves all pages of results.
|
|
263
|
+
limit (int, optional): Number of results per page (max 1000).
|
|
264
|
+
start (int, optional): Starting offset for pagination.
|
|
265
|
+
**extra_params (Any): Any additional parameters to include in the query.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List[purchaseOrder]: List of wrapped purchase order resources.
|
|
269
|
+
"""
|
|
270
|
+
return self.client._query(
|
|
271
|
+
endpoint=self.endpoint,
|
|
272
|
+
resource_cls=purchaseOrder,
|
|
273
|
+
query=query,
|
|
274
|
+
sort=sort,
|
|
275
|
+
filter=filter,
|
|
276
|
+
all=all,
|
|
277
|
+
limit=limit,
|
|
278
|
+
start=start,
|
|
279
|
+
**extra_params
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
class purchaseOrder(APIResource[PurchaseOrder]):
|
|
283
|
+
endpoint = "purchasing/orders"
|
|
284
|
+
Model = PurchaseOrder
|
|
285
|
+
|
|
286
|
+
def issue(self) -> 'purchaseOrder':
|
|
287
|
+
"""
|
|
288
|
+
Issue this purchase order.
|
|
289
|
+
|
|
290
|
+
Sends a POST request to Spire to change the purchase order's status to
|
|
291
|
+
“I” (Issued). Issuing an order typically means it has been confirmed
|
|
292
|
+
and is ready to be sent to the vendor.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
purchaseOrder: The updated purchase order object with the status set to "I" (Issued).
|
|
296
|
+
|
|
297
|
+
Raise:
|
|
298
|
+
CreateRequestError: If the request fails or the API returns an error status.
|
|
299
|
+
"""
|
|
300
|
+
response = self._client._post(f"/{self.endpoint}/{str(self.id)}/issue")
|
|
301
|
+
if response.get("status_code") != 200:
|
|
302
|
+
raise CreateRequestError(f"Failed to issue purchase order: {self.id}: {response.text}")
|
|
303
|
+
return purchaseOrder.from_json(response, self._client)
|
|
304
|
+
|
|
305
|
+
def delete(self) -> bool:
|
|
306
|
+
"""
|
|
307
|
+
Cancels and deletes this purchase order.
|
|
308
|
+
|
|
309
|
+
Sends a DELETE request to the API to remove the purchase order with the current ID.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
bool: True if the order was successfully deleted (HTTP 204 or 200), False otherwise.
|
|
313
|
+
"""
|
|
314
|
+
return self._client._delete(f"/{self.endpoint}/{str(self.id)}")
|
|
315
|
+
|
|
316
|
+
def update(self, order: "purchaseOrder" = None) -> 'purchaseOrder':
|
|
317
|
+
"""
|
|
318
|
+
Update this sales order.
|
|
319
|
+
|
|
320
|
+
If no purchase order object is provided, updates the current instance on the server.
|
|
321
|
+
If a purchase order object is provided, updates the purchase order using the given data.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
order (purchaseOrder, optional): An optional purchaseOrder instance to use for the update.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
purchaseOrder: The updated purchaseOrder object reflecting the new status.
|
|
328
|
+
"""
|
|
329
|
+
data = order.model_dump(exclude_unset=True, exclude_none=True) if order else self.model_dump(exclude_unset=True, exclude_none=True)
|
|
330
|
+
response = self._client._put(f"/{self.endpoint}/{str(self.id)}", json=data)
|
|
331
|
+
return purchaseOrder.from_json(response, self._client)
|
|
332
|
+
|
|
333
|
+
def receive(self, receiveAll: bool = None) -> 'purchaseOrder':
|
|
334
|
+
"""
|
|
335
|
+
Receive this purchase order.
|
|
336
|
+
|
|
337
|
+
Sends a POST request to Spire to change the purchase order's status to
|
|
338
|
+
"R" (Received). Receiving an order typically means the received quantites have been
|
|
339
|
+
entered and the order is ready to be invoiced.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
receiveAll (bool, optional): An optional boolean to recieve all quantites on the purchase order.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
purchaseOrder: The updated purchase order object with the status set to "R" (Received).
|
|
346
|
+
|
|
347
|
+
Raise:
|
|
348
|
+
CreateRequestError: If the request fails or the API returns an error status.
|
|
349
|
+
"""
|
|
350
|
+
if receiveAll:
|
|
351
|
+
for item in self.model.items:
|
|
352
|
+
item.receiveQty = item.orderQty
|
|
353
|
+
self.update()
|
|
354
|
+
response = self._client._post(f"/{self.endpoint}/{str(self.id)}/receive")
|
|
355
|
+
if response.get('status_code') != 200:
|
|
356
|
+
raise CreateRequestError(f"Failed to receive purchase order: {self.id}. (Check Order Quantites): {response.text}")
|
|
357
|
+
return purchaseOrder.from_json(response, self._client)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
@@ -2,6 +2,7 @@ from .client import SpireClient
|
|
|
2
2
|
from .sales import OrdersClient, InvoiceClient
|
|
3
3
|
from .customers import CustomerClient
|
|
4
4
|
from .inventory import InventoryClient
|
|
5
|
+
from .purchasing import PurchasingClient, PurchasingHistoryClient
|
|
5
6
|
|
|
6
7
|
class Spire:
|
|
7
8
|
"""
|
|
@@ -16,6 +17,8 @@ class Spire:
|
|
|
16
17
|
invoices (InvoiceClient): Client for accessing invoices.
|
|
17
18
|
customers (CustomerClient): Client for accessing customer records.
|
|
18
19
|
inventory (InventoryClient): Client for accessing inventory items.
|
|
20
|
+
purchasing (PurchasingClient): Client for accessing purchasing records.
|
|
21
|
+
purchasingHistory (PurchasingHistoryClient): Client for accessing purchasing history records.
|
|
19
22
|
"""
|
|
20
23
|
def __init__(self, host : str, company : str, username : str, password : str):
|
|
21
24
|
"""
|
|
@@ -32,5 +35,7 @@ class Spire:
|
|
|
32
35
|
self.invoices = InvoiceClient(self.client)
|
|
33
36
|
self.customers = CustomerClient(self.client)
|
|
34
37
|
self.inventory = InventoryClient(self.client)
|
|
38
|
+
self.purchasing = PurchasingClient(self.client)
|
|
39
|
+
self.purchasingHistory = PurchasingHistoryClient(self.client)
|
|
35
40
|
|
|
36
41
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spyreapi
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
4
4
|
Summary: A robust and extensible Python client for interacting with the [Spire Business Software API](https://developer.spiresystems.com/reference). This client provides an object-oriented interface to get, create, update, delete, query, filter, sort, and manage various Spire modules such as Sales Orders, Invoices, Inventory Items, and more.
|
|
5
5
|
Author-email: Sanjid Sharaf <sanjidsharaf1@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,12 +8,14 @@ src/spyre/client.py
|
|
|
8
8
|
src/spyre/crm.py
|
|
9
9
|
src/spyre/customers.py
|
|
10
10
|
src/spyre/inventory.py
|
|
11
|
+
src/spyre/purchasing.py
|
|
11
12
|
src/spyre/sales.py
|
|
12
13
|
src/spyre/spire.py
|
|
13
14
|
src/spyre/utils.py
|
|
14
15
|
src/spyre/Models/__init__.py
|
|
15
16
|
src/spyre/Models/customers_models.py
|
|
16
17
|
src/spyre/Models/inventory_models.py
|
|
18
|
+
src/spyre/Models/purchasing_models.py
|
|
17
19
|
src/spyre/Models/sales_models.py
|
|
18
20
|
src/spyre/Models/shared_models.py
|
|
19
21
|
src/spyreapi.egg-info/PKG-INFO
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|