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.
Files changed (27) hide show
  1. {spyreapi-0.0.7/src/spyreapi.egg-info → spyreapi-0.0.8}/PKG-INFO +1 -1
  2. {spyreapi-0.0.7 → spyreapi-0.0.8}/pyproject.toml +1 -1
  3. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/inventory_models.py +1 -7
  4. spyreapi-0.0.8/src/spyre/Models/purchasing_models.py +77 -0
  5. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/shared_models.py +8 -1
  6. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/__init__.py +11 -0
  7. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/client.py +6 -3
  8. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/inventory.py +5 -5
  9. spyreapi-0.0.8/src/spyre/purchasing.py +363 -0
  10. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/spire.py +5 -0
  11. {spyreapi-0.0.7 → spyreapi-0.0.8/src/spyreapi.egg-info}/PKG-INFO +1 -1
  12. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyreapi.egg-info/SOURCES.txt +2 -0
  13. {spyreapi-0.0.7 → spyreapi-0.0.8}/LICENSE +0 -0
  14. {spyreapi-0.0.7 → spyreapi-0.0.8}/MANIFEST.in +0 -0
  15. {spyreapi-0.0.7 → spyreapi-0.0.8}/README.md +0 -0
  16. {spyreapi-0.0.7 → spyreapi-0.0.8}/setup.cfg +0 -0
  17. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Exceptions.py +0 -0
  18. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/__init__.py +0 -0
  19. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/customers_models.py +0 -0
  20. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/Models/sales_models.py +0 -0
  21. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/crm.py +0 -0
  22. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/customers.py +0 -0
  23. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/sales.py +0 -0
  24. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyre/utils.py +0 -0
  25. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyreapi.egg-info/dependency_links.txt +0 -0
  26. {spyreapi-0.0.7 → spyreapi-0.0.8}/src/spyreapi.egg-info/requires.txt +0 -0
  27. {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.7
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spyreapi"
7
- version = "0.0.7"
7
+ version = "0.0.8"
8
8
  authors = [
9
9
  { name="Sanjid Sharaf", email="sanjidsharaf1@gmail.com" },
10
10
  ]
@@ -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
- self.base_url = f"https://{host}/api/v2/companies/{company}"
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
- uom (uom): A `uom` instance with updated field values.
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
- upc (upc): A `upc` instance with updated field values.
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
- uom (UnitOfMeasure): The UnitOfMeasure Pydantic model to be created.
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
- uom_record (UPC): The UPC Pydantic model to be created.
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
- upc (upc): A `upc` instance with updated field values.
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.7
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